Skip to content

Commit e301317

Browse files
authored
[URI] Allows regular expressions as ID constraint. (#75)
* Fixes rubocop config. * Allows regex in id attributes. * Cleanup * Adds example to readme * Adds missing specs
1 parent 8c7f984 commit e301317

5 files changed

Lines changed: 147 additions & 28 deletions

File tree

.rubocop.yml

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,27 @@
22
# require: rubocop-performance
33

44
AllCops:
5-
TargetRubyVersion: 2.5
6-
Exclude:
7-
- db/schema.rb
5+
TargetRubyVersion: 2.6
6+
NewCops: enable
87

9-
CaseIndentation:
8+
Layout/CaseIndentation:
109
EnforcedStyle: end
1110

12-
CollectionMethods:
11+
Style/CollectionMethods:
1312
PreferredMethods:
1413
reduce: inject
1514
inject: inject
1615

17-
Lambda:
16+
Style/Lambda:
1817
Enabled: false
1918

20-
Layout/AlignArguments:
19+
Layout/ArgumentAlignment:
2120
EnforcedStyle: with_fixed_indentation
2221

23-
Layout/AlignHash:
22+
Layout/HashAlignment:
2423
EnforcedLastArgumentHashStyle: ignore_implicit
2524

26-
Layout/AlignParameters:
25+
Layout/ParameterAlignment:
2726
EnforcedStyle: with_fixed_indentation
2827

2928
Layout/EmptyLinesAroundBlockBody:
@@ -35,7 +34,7 @@ Layout/EmptyLinesAroundClassBody:
3534
Layout/EmptyLinesAroundModuleBody:
3635
Enabled: false
3736

38-
Layout/IndentFirstHashElement:
37+
Layout/FirstHashElementIndentation:
3938
EnforcedStyle: consistent
4039

4140
Layout/MultilineOperationIndentation:
@@ -50,13 +49,13 @@ Layout/SpaceInLambdaLiteral:
5049
Layout/EndAlignment:
5150
EnforcedStyleAlignWith: variable
5251

53-
MethodLength:
52+
Metrics/MethodLength:
5453
Max: 24
5554

5655
Metrics/BlockLength:
57-
ExcludedMethods: [it, describe, context, feature, freeze, specify, define, renum]
56+
IgnoredMethods: [it, describe, context, feature, freeze, specify, define, renum]
5857

59-
Metrics/LineLength:
58+
Layout/LineLength:
6059
Max: 120
6160

6261
Style/AndOr:
@@ -71,5 +70,5 @@ Style/Documentation:
7170
Style/EachWithObject:
7271
Enabled: false
7372

74-
TrailingBlankLines:
73+
Layout/TrailingEmptyLines:
7574
Enabled: false

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ InvoicesType = InvoiceURI::Types::URI | InvoiceDuplicateURI::Types::URI
7777
InvoicesType[InvoiceURI.new(id: 'final')] # => <#InvoiceURI 'com.example:billing:invoice:final'>
7878
InvoicesType[InvoiceDuplicateURI.new(id: 'final')] # => <#InvoiceDuplicateURI 'com.example:billing:invoice:final'>
7979
InvoicesType[InvoiceURI.new(id: 'pro_forma')] # => Dry::Types::ConstraintError
80+
81+
# regular expressions for `id` are also supported
82+
InvoiceRegexURI = Studitemps::Utils::URI.build(from: InvoiceURI, id: /I-\d{3}/)
83+
84+
InvoiceRegexURI.build('com.example:billing:invoice:pro_forma') # => Studitemps::Utils::URI::Base::InvalidURI
85+
InvoiceRegexURI.new(id: 'I-123') # => #<InvoiceRegexURI 'com.example:billing:invoice:I-123'>
86+
InvoiceRegexURI.new(id: 'pro_forma') # => Dry::Types::ConstraintError
8087
```
8188

8289
### Extensions

lib/studitemps/utils/uri/builder.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,11 @@ def value_regex(value, klass, default: '[\w\-_]+')
7979
end
8080

8181
def enum_regex(value, klass, default: '[\w\-_]+')
82-
return default unless klass.send(value)
82+
return default unless (values = klass.send(value))
83+
return values if values.is_a? Regexp
8384

84-
values = Array(klass.send(value)).map { |v| Regexp.escape(v) }
85-
"(#{values.join('|')})"
85+
escaped_values = Array(values).map { |v| Regexp.escape(v) }
86+
"(#{escaped_values.join('|')})"
8687
end
8788

8889
def default_type

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ module Types
3939
##
4040
# Adds type checks to initializer arguments.
4141
# `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).
42+
# values (enum if URI builder is provided with an array as the argument). `id` also supports regular expressions.
4343
#
4444
# @example
4545
# require 'studitemps/utils/uri/extensions/types'
@@ -55,31 +55,47 @@ class Builder
5555
private
5656

5757
def schema_type(klass)
58-
value_type(:schema, klass)
58+
value_type(klass.schema)
5959
end
6060

6161
def context_type(klass)
62-
value_type(:context, klass)
62+
value_type(klass.context)
6363
end
6464

6565
def resource_type(klass)
66-
enum_type(:resource, klass)
66+
enum_type(klass.resource)
6767
end
6868

6969
def id_type(klass)
70-
enum_type(:id, klass)
70+
dynamic_type(klass.id)
7171
end
7272

73-
def value_type(value, klass, default: default_type)
74-
return default unless klass.send(value)
73+
def dynamic_type(value)
74+
case value
75+
when Array then enum_type(value)
76+
when String, Symbol then value_type(value)
77+
when Regexp then regexp_type(value)
78+
else
79+
default_type
80+
end
81+
end
82+
83+
def value_type(value)
84+
return default_type unless value
85+
86+
Dry.Types::Value(value)
87+
end
88+
89+
def enum_type(value)
90+
return default_type unless value
7591

76-
Dry.Types::Value(klass.send(value))
92+
Dry.Types::Strict::String.enum(*Array(value))
7793
end
7894

79-
def enum_type(value, klass, default: default_type)
80-
return default unless klass.send(value)
95+
def regexp_type(value)
96+
return default_type unless value
8197

82-
Dry.Types::Strict::String.enum(*Array(klass.send(value)))
98+
Dry.Types::Strict::String.constrained(format: value)
8399
end
84100
end
85101
end

spec/studitemps/uri_spec.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,20 @@ module Utils # rubocop:disable Metrics/ModuleLength
3434
expect(klass.resource).to eq 'order'
3535
end
3636

37+
it 'for single ID' do
38+
klass = URI.build(schema: 'com.example', context: 'billing', resource: 'invoice', id: 'final')
39+
expect(klass.id).to eq 'final'
40+
end
41+
3742
it 'for a list of IDs' do
3843
klass = URI.build(schema: 'com.example', context: 'billing', resource: 'invoice', id: %w[final past_due])
3944
expect(klass.id).to eq %w[final past_due]
4045
end
46+
47+
it 'for regex' do
48+
klass = URI.build(schema: 'com.example', context: 'billing', resource: 'invoice', id: /I-\d{3}/)
49+
expect(klass.id).to eq(/I-\d{3}/)
50+
end
4151
end
4252

4353
describe 'URI regex' do
@@ -73,6 +83,18 @@ module Utils # rubocop:disable Metrics/ModuleLength
7383
it { is_expected.to match 'com.example:billing:invoice:final' }
7484
it { is_expected.to_not match 'com.example:billing:invoice:pro_forma' }
7585
end
86+
87+
context 'custom regex' do
88+
subject(:regex) {
89+
URI.build(schema: 'com.example', context: 'billing', resource: 'invoice', id: /I-\d{3}/)::REGEX
90+
}
91+
92+
it { is_expected.to match 'com.example:billing:invoice:I-123' }
93+
it { is_expected.to_not match 'com.example:billing:invoice:i-123' }
94+
it { is_expected.to_not match 'com.example:billing:invoice:123' }
95+
it { is_expected.to_not match 'com.example:billing:invoice:I-abc' }
96+
it { is_expected.to_not match 'com.example:billing:invoice:I-1234' }
97+
end
7698
end
7799

78100
describe 'URI instance' do
@@ -308,6 +330,44 @@ module Utils # rubocop:disable Metrics/ModuleLength
308330

309331
end
310332

333+
describe 'Fixed URI' do
334+
subject(:klass) {
335+
URI.build(
336+
schema: 'com.example',
337+
context: 'billing',
338+
resource: 'invoice',
339+
id: 'final'
340+
)
341+
}
342+
343+
it 'only accepts fixed URI' do
344+
uri = klass.new(
345+
schema: 'com.example',
346+
context: 'billing',
347+
resource: 'invoice',
348+
id: 'final'
349+
)
350+
351+
expect(klass.new(id: 'final')).to eq uri
352+
353+
expect {
354+
klass.new(id: 'not_final')
355+
}.to raise_error Dry::Types::ConstraintError
356+
357+
expect {
358+
klass.new(resource: 'not_invoice', id: 'final')
359+
}.to raise_error Dry::Types::ConstraintError
360+
361+
expect {
362+
klass.new(context: 'not_billing', id: 'final')
363+
}.to raise_error Dry::Types::ConstraintError
364+
365+
expect {
366+
klass.new(schema: 'not_example.com', id: 'final')
367+
}.to raise_error Dry::Types::ConstraintError
368+
end
369+
end
370+
311371
describe 'Attribute Types' do
312372
subject(:klass) {
313373
URI.build(
@@ -349,6 +409,42 @@ module Utils # rubocop:disable Metrics/ModuleLength
349409
}.to raise_error Dry::Types::ConstraintError
350410
end
351411
end
412+
413+
describe 'Regexp' do
414+
let(:invoice_klass) { URI.build(from: klass, resource: 'invoice', id: /I-\d{3}/) }
415+
416+
it 'validates input' do
417+
expect {
418+
invoice_klass.new(id: 'I-123')
419+
}.to_not raise_error
420+
421+
expect {
422+
invoice_klass.new(id: 'X-123')
423+
}.to raise_error Dry::Types::ConstraintError
424+
end
425+
426+
it 'has uri type' do
427+
type = invoice_klass::Types::URI
428+
uri = invoice_klass.new(id: 'I-123')
429+
430+
expect(type[uri]).to eq uri
431+
expect(type['com.example:billing:invoice:I-123']).to eq uri
432+
expect {
433+
type['com.example:billing:invoice:<other>']
434+
}.to raise_error Dry::Types::CoercionError
435+
end
436+
437+
it 'has string type' do
438+
type = invoice_klass::Types::String
439+
uri = invoice_klass.new(id: 'I-123')
440+
441+
expect(type[uri]).to eq uri.to_s
442+
expect(type['com.example:billing:invoice:I-123']).to eq uri.to_s
443+
expect {
444+
type['com.example:billing:invoice:<other>']
445+
}.to raise_error Dry::Types::CoercionError
446+
end
447+
end
352448
end
353449
end
354450
end

0 commit comments

Comments
 (0)