Skip to content

Commit 5c369ee

Browse files
authored
[URI] Adds support for lists of contexts and ids. (#5)
It is now possible to create URIs with a given list of possible context and ids.
1 parent a717237 commit 5c369ee

6 files changed

Lines changed: 216 additions & 36 deletions

File tree

README.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,27 @@ gem 'studitemps-utils'
2121

2222
And then execute:
2323

24-
$ bundle
24+
```shell
25+
$ bundle
26+
```
2527

2628
Or install it yourself as:
2729

28-
$ gem install studitemps-utils
30+
```shell
31+
$ gem install studitemps-utils
32+
```
2933

3034
## URI
3135

3236
An Studitemps Utils URI references similar to a normal URI a specific resource. It contains at least a `schema` but most
3337
of the time when used to reference a resource it also has a `context`, `resource`, and an `id`.
3438

3539
Example: `com.example:billing:invoice:R422342`
36-
- schema: `com.example` - Some kind of schema to make URI globally unique.
37-
- context: `billing` - The context the URI (and the resource) belongs to.
38-
- resource: `invoice` - The resource type.
39-
- id: `R422342` - The resource id.
40+
41+
- schema: `com.example` - Some kind of schema to make URI globally unique.
42+
- context: `billing` - The context the URI (and the resource) belongs to.
43+
- resource: `invoice` - The resource type.
44+
- id: `R422342` - The resource id.
4045

4146
### Usage
4247

@@ -52,6 +57,18 @@ uri.to_s # => 'com.example:billing:invoice:R422342'
5257
InvoiceURI.build('com.example:billing:invoice:R422342') # => #<InvoiceURI 'com.example:billing:invoice:R422342'>
5358
InvoiceURI.build(id: 'R422342') # => #<InvoiceURI 'com.example:billing:invoice:R422342'>
5459
InvoiceURI.build(uri) # => #<InvoiceURI 'com.example:billing:invoice:R422342'>
60+
61+
# `resource` and `id` supports array so that `.build` will verify every value for a given string.
62+
# If we also want to check the initializer we can use the "types" extension to do so:
63+
require 'studitemps/utils/uri/extensions/types'
64+
65+
InvoiceURI = Studitemps::Utils::URI.build(
66+
schema: 'com.example', context: 'billing', resource: 'invoice', id: %w[final past_due]
67+
)
68+
69+
InvoiceURI.build('com.example:billing:invoice:pro_forma') # => Studitemps::Utils::URI::Base::InvalidURI
70+
InvoiceURI.new(id: 'final') # => #<InvoiceURI 'com.example:billing:invoice:final'>
71+
InvoiceURI.new(id: 'pro_forma') # => Dry::Types::ConstraintError
5572
```
5673

5774
### Extensions
@@ -75,11 +92,12 @@ MyBaseURI.load('com.example:billing:invoice:R422342') # => #<MyBaseURI 'com.exa
7592
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
7693

7794
To install this gem onto your local machine, run `bundle exec rake install`.
95+
7896
<!-- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). -->
7997

8098
## Contributing
8199

82-
Bug reports and pull requests are welcome on GitHub at https://github.com/STUDITEMPS/studitemps-utils.
100+
Bug reports and pull requests are welcome on GitHub at <https://github.com/STUDITEMPS/studitemps-utils>.
83101

84102
## License
85103

lib/studitemps/utils/uri.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ module URI
4141
#
4242
# ExampleURI.build('com.example:billing:invoice:R422342')
4343
# # => #<ExampleURI 'com.example:billing:invoice:R422342'>
44-
def build(schema: nil, context: nil, resource: nil, from: Base)
45-
Builder.new.call(schema: schema, context: context, resource: resource, superclass: from)
44+
def build(schema: nil, context: nil, resource: nil, id: nil, from: Base)
45+
Builder.new.call(schema: schema, context: context, resource: resource, id: id, superclass: from)
4646
end
4747
end
4848
end

lib/studitemps/utils/uri/base.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ class InvalidURI < StandardError; end
1414
defines :schema
1515
defines :context
1616
defines :resource
17+
defines :id
18+
19+
# @!parse
20+
# # Regular expression which the string representation of the URI has to match.
21+
# # `value` - matches the exact value
22+
# # `enum` - matches any of the given values
23+
# REGEX = /\A(?<schema>`value`)\:(?<context>`value`)(:(?<resource>`enum`)(:(?<id>`enum`))?)?\z/
1724

1825
# Returns string representation of URI.
1926
#

lib/studitemps/utils/uri/builder.rb

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ class Builder
1515
#
1616
# @param [String, nil] schema the schema part of the new URI
1717
# @param [String, nil] context the context part of the new URI
18-
# @param [String, nil] resource the resource part of the new URI
18+
# @param [String, [String], nil] resource the resource part of the new URI
19+
# @param [String, [String], nil] id the optional fixed id part of the new URI
1920
# @param [String] superclass uri base class
2021
# @return [Base] the new URI class
2122
#
2223
# @note Use {URI.build} instead to create new URI classes
2324
# @api private
24-
def call(schema: nil, context: nil, resource: nil, superclass: ::Studitemps::Utils::URI::Base)
25+
def call(schema: nil, context: nil, resource: nil, id: nil, superclass: ::Studitemps::Utils::URI::Base)
2526
raise ArgumentError, 'missing schema' if superclass == ::Studitemps::Utils::URI::Base && schema.nil?
2627

2728
klass = build_class(superclass)
28-
configure_class(klass, schema, context, resource)
29+
configure_class(klass, schema, context, resource, id)
2930
build_regex(klass)
3031
build_initializer(klass)
3132
run_extensions(klass)
@@ -38,41 +39,61 @@ def build_class(superclass)
3839
Class.new(superclass)
3940
end
4041

41-
def configure_class(klass, new_schema, new_context, new_resource)
42+
def configure_class(klass, new_schema, new_context, new_resource, new_id)
4243
klass.class_eval do
4344
schema new_schema if new_schema
4445
context new_context if new_context
4546
resource new_resource if new_resource
47+
id new_id if new_id
4648
end
4749
end
4850

49-
def build_regex(klass, default: '[\w\-_]+')
50-
schema = Regexp.escape(klass.schema)
51-
context = klass.context ? Regexp.escape(klass.context) : default
52-
resource = klass.resource ? Regexp.escape(klass.resource) : default
51+
def build_regex(klass)
5352
regex = /\A
54-
(?<schema>#{schema})\:
55-
(?<context>#{context})
56-
(:(?<resource>#{resource})
57-
(:(?<id>.*))?)?
53+
(?<schema>#{value_regex(:schema, klass)})\:
54+
(?<context>#{value_regex(:context, klass)})
55+
(:(?<resource>#{enum_regex(:resource, klass)})
56+
(:(?<id>#{enum_regex(:id, klass, default: '.*')}))?)?
5857
\z/x
5958
klass.const_set('REGEX', regex.freeze)
6059
end
6160

6261
def build_initializer(klass) # rubocop:disable Metrics/AbcSize
63-
klass.class_eval do
64-
include Dry::Initializer[undefined: false].define -> {
65-
option :schema, proc(&:to_s), optional: false, default: -> { klass.schema }
66-
option :context, proc(&:to_s), optional: true, default: -> { klass.context }
67-
option :resource, proc(&:to_s), optional: true, default: -> { klass.resource }
68-
option :id, proc(&:to_s), optional: true
69-
}
70-
end
62+
klass.extend Dry::Initializer[undefined: false]
63+
64+
klass.option :schema, type: schema_type(klass), optional: false, default: -> { klass.schema }
65+
klass.option :context, type: context_type(klass), optional: true, default: -> { klass.context }
66+
klass.option :resource, type: resource_type(klass), optional: true,
67+
default: -> { Array(klass.resource).first }
68+
klass.option :id, type: id_type(klass), optional: true
7169
end
7270

7371
def run_extensions(klass)
7472
self.class.extensions.each { |extension| extension.call(klass) }
7573
end
74+
75+
def value_regex(value, klass, default: '[\w\-_]+')
76+
return default unless klass.send(value)
77+
78+
Regexp.escape(klass.send(value))
79+
end
80+
81+
def enum_regex(value, klass, default: '[\w\-_]+')
82+
return default unless klass.send(value)
83+
84+
values = Array(klass.send(value)).map { |v| Regexp.escape(v) }
85+
"(#{values.join('|')})"
86+
end
87+
88+
def default_type
89+
proc(&:to_s)
90+
end
91+
92+
%i[schema context resource id].each do |attribute|
93+
define_method "#{attribute}_type" do |_klass|
94+
default_type
95+
end
96+
end
7697
end
7798
end
7899
end

lib/studitemps/utils/uri/extensions/types.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,54 @@ module Types
3232
types.const_set 'String', Dry.Types.Constructor(klass) { |value| klass.build(value).to_s }
3333
klass.const_set 'Types', types
3434
}
35+
36+
end
37+
end
38+
39+
##
40+
# Adds type checks to initializer arguments.
41+
# `schema` and `context` values are checked and `resource` and `id` are checked against a possible list of
42+
# values (enum if URI builder is provided with an array as the argument).
43+
#
44+
# @example
45+
# require 'studitemps/utils/uri/extensions/types'
46+
# InvoiceURI = Studitemps::Utils::URI.build(
47+
# schema: 'com.example', context: 'billing', resource: 'invoice', id: %w[final past_due]
48+
# )
49+
#
50+
# InvoiceURI.new(id: 'final') # => #<InvoiceURI 'com.example:billing:invoice:final'>
51+
# InvoiceURI.new(id: 'pro_forma') # => Dry::Types::ConstraintError
52+
#
53+
# @since 0.2.0
54+
class Builder
55+
private
56+
57+
def schema_type(klass)
58+
value_type(:schema, klass)
59+
end
60+
61+
def context_type(klass)
62+
value_type(:context, klass)
63+
end
64+
65+
def resource_type(klass)
66+
enum_type(:resource, klass)
67+
end
68+
69+
def id_type(klass)
70+
enum_type(:id, klass)
71+
end
72+
73+
def value_type(value, klass, default: default_type)
74+
return default unless klass.send(value)
75+
76+
Dry.Types::Value(klass.send(value))
77+
end
78+
79+
def enum_type(value, klass, default: default_type)
80+
return default unless klass.send(value)
81+
82+
Dry.Types::Strict::String.enum(*Array(klass.send(value)))
3583
end
3684
end
3785
end

0 commit comments

Comments
 (0)