nrser 0.0.24 → 0.0.25

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,43 +36,97 @@ module NRSER::Types
36
36
  def attrs attrs, options = {}
37
37
  Attrs.new attrs, **options
38
38
  end
39
-
39
+
40
+
41
+ # @overload length exact, options = {}
42
+ # Get a length attribute type that specifies an `exact` value.
43
+ #
44
+ # @example
45
+ # only_type = NRSER::Types.length 1
46
+ #
47
+ # only_type.test []
48
+ # # => false
49
+ #
50
+ # only_type.test [:x]
51
+ # # => true
52
+ #
53
+ # only_type.test [:x, :y]
54
+ # # => false
55
+ #
56
+ # @param [Integer] exact
57
+ # Exact non-negative integer that the length must be to satisfy the
58
+ # type created.
59
+ #
60
+ # @param [Hash] options
61
+ # Options hash passed up to {NRSER::Types::Type} constructor.
62
+ #
63
+ # @return [NRSER::Types::Attrs]
64
+ # Type satisfied by a `#length` attribute that is exactly `exact`.
65
+ #
66
+ #
67
+ # @overload length bounds, options = {}
68
+ # Get a length attribute type satisfied by values within a `:min` and
69
+ # `:max` (inclusive).
70
+ #
71
+ # @example
72
+ # three_to_five = NRSER::Types.length( {min: 3, max: 5}, name: '3-5' )
73
+ # three_to_five.test [1, 2] # => false
74
+ # three_to_five.test [1, 2, 3] # => true
75
+ # three_to_five.test [1, 2, 3, 4] # => true
76
+ # three_to_five.test [1, 2, 3, 4, 5] # => true
77
+ # three_to_five.test [1, 2, 3, 4, 5, 6] # => false
78
+ #
79
+ # @param [Hash] bounds
80
+ #
81
+ # @option bounds [Integer] :min
82
+ # An optional minimum value that the `#length` should not be less than.
83
+ #
84
+ # @option bounds [Integer] :max
85
+ # An optional maximum value that the `#length` should not be more than.
86
+ #
87
+ # @option bounds [Integer] :length
88
+ # An optional value for both the minimum and maximum.
89
+ #
90
+ # @param [Hash] options
91
+ # Options hash passed up to {NRSER::Types::Type} constructor.
92
+ #
93
+ # @return [NRSER::Types::Attrs]
94
+ # Type satisfied by a `#length` attribute between the `:min` and `:max`
95
+ # (inclusive).
96
+ #
40
97
  def length *args
41
98
  bounds = {}
42
- options = {}
99
+ options = if args[1].is_a?( Hash ) then args[1] else {} end
43
100
 
44
- case args.length
45
- when 1
46
- case args[0]
47
- when ::Integer
48
- bounds[:min] = bounds[:max] = non_neg_int.check(args[0])
49
-
50
- when ::Hash
51
- options = NRSER.symbolize_keys args[0]
52
-
53
- bounds[:min] = options.delete :min
54
- bounds[:max] = options.delete :max
55
-
56
- if length = options.delete(:length)
57
- bounds[:min] = length
58
- bounds[:max] = length
59
- end
60
-
61
- else
62
- raise ArgumentError, <<-END.squish
63
- arg must be positive integer or option hash, found:
64
- #{ args[0].inspect } of type #{ args[0].class }
65
- END
101
+ case args[0]
102
+ when ::Integer
103
+ # It's just a length
104
+ bounds[:min] = bounds[:max] = non_neg_int.check args[0]
105
+
106
+ when ::Hash
107
+ # It's keyword args
108
+ kwds = NRSER.symbolize_keys args[0]
109
+
110
+ # Pull any :min and :max in the keywords
111
+ bounds[:min] = kwds.delete :min
112
+ bounds[:max] = kwds.delete :max
113
+
114
+ # But override with :length if we got it
115
+ if length = kwds.delete(:length)
116
+ bounds[:min] = length
117
+ bounds[:max] = length
66
118
  end
67
119
 
68
- when 2
69
- bounds[:min] = bounds[:max] = non_neg_int.check(args[0])
70
- options = args[1]
120
+ # (Reverse) merge anything else into the options (options hash values
121
+ # take precedence)
122
+ options = kwds.merge options
71
123
 
72
- else
124
+ else
73
125
  raise ArgumentError, <<-END.squish
74
- must provided 1 or 2 args.
126
+ arg must be positive integer or option hash, found:
127
+ #{ args[0].inspect } of type #{ args[0].class }
75
128
  END
129
+
76
130
  end
77
131
 
78
132
  bounded_type = bounded bounds
@@ -90,7 +144,6 @@ module NRSER::Types
90
144
  attrs({ length: length_type }, options)
91
145
  end # #length
92
146
 
93
-
94
147
  end # class << self (Eigenclass)
95
148
 
96
149
  end # NRSER::Types
@@ -8,30 +8,32 @@ module NRSER::Types
8
8
  class Combinator < NRSER::Types::Type
9
9
  attr_reader :types
10
10
 
11
+
11
12
  def initialize *types, **options
12
13
  super **options
13
14
  @types = types.map {|type| NRSER::Types.make type}
14
15
  end
15
16
 
17
+
16
18
  def default_name
17
- @name || (
18
- "#{ self.class.short_name }<" +
19
- @types.map {|type| type.name }.join(',') +
20
- ">"
21
- )
19
+ "#{ self.class.short_name }<" +
20
+ @types.map {|type| type.name }.join(',') +
21
+ ">"
22
22
  end
23
23
 
24
+
24
25
  # a combinator may attempt to parse from a string if any of it's types
25
26
  # can do so
26
27
  def has_from_s?
27
28
  @types.any? {|type| type.has_from_s?}
28
29
  end
29
30
 
31
+
30
32
  # a combinator iterates through each of it's types, trying the
31
33
  # conversion and seeing if the result satisfies the combinator type
32
34
  # itself. the first such value found is returned.
33
35
  def from_s s
34
- @types.each {|type|
36
+ @types.each { |type|
35
37
  if type.respond_to? :from_s
36
38
  begin
37
39
  return check type.from_s(s)
@@ -45,6 +47,52 @@ module NRSER::Types
45
47
  "none of combinator #{ self.to_s } types could convert #{ s.inspect }"
46
48
  end
47
49
 
50
+
51
+ # Overridden to delegate functionality to the combined types:
52
+ #
53
+ # A combinator may attempt to parse from a string if any of it's types
54
+ # can do so.
55
+ #
56
+ # @return [Boolean]
57
+ #
58
+ def has_from_s?
59
+ @types.any? {|type| type.has_from_s?}
60
+ end # has_from_s
61
+
62
+
63
+ # Overridden to delegate functionality to the combined types:
64
+ #
65
+ # A combinator can convert a value to data if *any* of it's types can.
66
+ #
67
+ # @return [Boolean]
68
+ #
69
+ def has_to_data?
70
+ @types.any? { |type| type.has_to_data? }
71
+ end # #has_to_data
72
+
73
+
74
+ # Overridden to delegate functionality to the combined types:
75
+ #
76
+ # The first of the combined types that responds to `#to_data` is used to
77
+ # dump the value.
78
+ #
79
+ # @param [Object] value
80
+ # Value of this type (though it is *not* checked).
81
+ #
82
+ # @return [Object]
83
+ # The data representation of the value.
84
+ #
85
+ def to_data value
86
+ @types.each { |type|
87
+ if type.respond_to? :to_data
88
+ return type.to_data value
89
+ end
90
+ }
91
+
92
+ raise NoMethodError, "#to_data not defined"
93
+ end # #to_data
94
+
95
+
48
96
  def == other
49
97
  equal?(other) || (
50
98
  other.class == self.class && other.types == @types
@@ -34,7 +34,8 @@ module NRSER::Types
34
34
  PATHNAME = is_a \
35
35
  Pathname,
36
36
  name: 'PathnameType',
37
- from_s: ->(string) { Pathname.new string }
37
+ from_s: ->(string) { Pathname.new string },
38
+ to_data: :to_s
38
39
 
39
40
 
40
41
  # A type satisfied by a {Pathname} instance that's not empty (meaning it's
@@ -53,7 +54,7 @@ module NRSER::Types
53
54
  #
54
55
  class << self
55
56
 
56
- def pathname **options
57
+ def pathname to_data: :to_s, **options
57
58
  if options.empty?
58
59
  PATHNAME
59
60
  else
@@ -61,11 +62,14 @@ module NRSER::Types
61
62
  Pathname,
62
63
  name: 'PathnameType',
63
64
  from_s: ->(string) { Pathname.new string },
65
+ to_data: to_data,
64
66
  **options
65
67
  end
66
68
  end
67
69
 
70
+ # A path is a non-empty {String} or {Pathname}.
68
71
  #
72
+ # @param **options see NRSER::Types::Type#initialize
69
73
  #
70
74
  # @return [NRSER::Types::Type]
71
75
  #
@@ -77,6 +81,62 @@ module NRSER::Types
77
81
  end
78
82
  end # #path
79
83
 
84
+
85
+ # An absolute {#path}.
86
+ #
87
+ # @param **options see NRSER::Types::Type#initialize
88
+ #
89
+ def abs_path name: 'AbsPath', **options
90
+ intersection \
91
+ path,
92
+ where { |path| File.absolute? path },
93
+ name: name,
94
+ **options
95
+ end
96
+
97
+
98
+ # A {NRSER::Types.path} that is a directory.
99
+ #
100
+ # @param [Hash] **options
101
+ # Construction options passed to {NRSER::Types::Type#initialize}.
102
+ #
103
+ # @return [NRSER::Types::Type]
104
+ #
105
+ def dir_path name: 'DirPath', **options
106
+ intersection \
107
+ path,
108
+ where { |path| File.directory? path },
109
+ name: name,
110
+ **options
111
+ end # #dir_path
112
+
113
+
114
+ # Absolute {.path} to a directory (both an {.abs_path} and an {.dir_path}).
115
+ #
116
+ # @param [type] name:
117
+ # @todo Add name param description.
118
+ #
119
+ # @return [return_type]
120
+ # @todo Document return value.
121
+ #
122
+ def abs_dir_path name: 'AbsDirPath', **options
123
+ intersection \
124
+ abs_path,
125
+ dir_path,
126
+ name: name,
127
+ **options
128
+ end # #abs_dir_path
129
+
130
+
131
+
132
+ def file_path name: 'FilePath', **options
133
+ intersection \
134
+ path,
135
+ where { |path| File.file? path },
136
+ name: name,
137
+ **options
138
+ end
139
+
80
140
  end # class << self (Eigenclass)
81
141
 
82
142
  end # module NRSER::Types
@@ -1,6 +1,13 @@
1
+ # Refinements
2
+ # =======================================================================
3
+
1
4
  require 'nrser/refinements'
2
5
  using NRSER
3
6
 
7
+
8
+ # Definitions
9
+ # =======================================================================
10
+
4
11
  module NRSER::Types
5
12
  class Type
6
13
  def self.short_name
@@ -25,9 +32,25 @@ module NRSER::Types
25
32
  # value that doesn't satisfy will result in a {TypeError} being raised
26
33
  # by {#from_s}.
27
34
  #
28
- def initialize name: nil, from_s: nil
35
+ # @param [nil | #call | #to_proc] to_data:
36
+ #
37
+ #
38
+ def initialize name: nil, from_s: nil, to_data: nil
29
39
  @name = name
30
40
  @from_s = from_s
41
+
42
+ @to_data = if to_data.nil?
43
+ nil
44
+ elsif to_data.respond_to?( :call )
45
+ to_data
46
+ elsif to_data.respond_to?( :to_proc )
47
+ to_data.to_proc
48
+ else
49
+ raise TypeError.squished <<-END
50
+ `to_data:` keyword arg must be `nil`, respond to `:call` or respond
51
+ to `:to_proc`; found #{ to_data.inspect }
52
+ END
53
+ end
31
54
  end # #initialize
32
55
 
33
56
 
@@ -43,6 +66,7 @@ module NRSER::Types
43
66
  raise NotImplementedError
44
67
  end
45
68
 
69
+
46
70
  def check value, &make_fail_message
47
71
  # success case
48
72
  return value if test value
@@ -58,14 +82,67 @@ module NRSER::Types
58
82
  raise TypeError.new msg
59
83
  end
60
84
 
85
+
86
+ # Overridden to customize behavior for the {#from_s} and {#to_data}
87
+ # methods - those methods are always defined, but we have {#respond_to?}
88
+ # return `false` if they lack the underlying instance variables needed
89
+ # to execute.
90
+ #
91
+ # @example
92
+ # t1 = t.where { |value| true }
93
+ # t1.respond_to? :from_s
94
+ # # => false
95
+ #
96
+ # t2 = t.where( from_s: ->(s){ s.split ',' } ) { |value| true }
97
+ # t2.respond_to? :from_s
98
+ # # => true
99
+ #
100
+ # @param [Symbol | String] name
101
+ # Method name to ask about.
102
+ #
103
+ # @param [Boolean] include_all
104
+ # IDK, part of Ruby API that is passed up to `super`.
105
+ #
106
+ # @return [Boolean]
107
+ #
61
108
  def respond_to? name, include_all = false
62
109
  if name == :from_s || name == 'from_s'
63
110
  has_from_s?
111
+ elsif name == :to_data || name == 'to_data'
112
+ has_to_data?
64
113
  else
65
114
  super name, include_all
66
115
  end
67
- end
116
+ end # #respond_to?
68
117
 
118
+
119
+ # Load a value of this type from a string representation by passing `s`
120
+ # to the {@from_s} {Proc}.
121
+ #
122
+ # Checks the value {@from_s} returns with {#check} before returning it, so
123
+ # you know it satisfies this type.
124
+ #
125
+ # @param [String] s
126
+ # String representation.
127
+ #
128
+ # @return [Object]
129
+ # Value that has passed {#check}.
130
+ #
131
+ # @raise [NoMethodError]
132
+ # If this type doesn't know how to load values from strings.
133
+ #
134
+ # In basic types this happens when {NRSER::Types::Type#initialize} was
135
+ # not provided a `from_s:` {Proc} argument.
136
+ #
137
+ # {NRSER::Types::Type} subclasses may override {#from_s} entirely,
138
+ # divorcing it from the `from_s:` constructor argument and internal
139
+ # {@from_s} instance variable (which is why {@from_s} is not publicly
140
+ # exposed - it should not be assumed to dictate {#from_s} behavior
141
+ # in general).
142
+ #
143
+ # @raise [TypeError]
144
+ # If the value loaded does not pass {#check}.
145
+ #
69
146
  def from_s s
70
147
  if @from_s.nil?
71
148
  raise NoMethodError, "#from_s not defined"
@@ -74,12 +151,55 @@ module NRSER::Types
74
151
  check @from_s.call( s )
75
152
  end
76
153
 
154
+
155
+ # Test if the type knows how to load values from strings.
156
+ #
157
+ # If this method returns `true`, then we expect {#from_s} to succeed.
158
+ #
159
+ # @return [Boolean]
160
+ #
77
161
  def has_from_s?
78
162
  ! @from_s.nil?
79
163
  end
80
164
 
165
+
166
+ # Test if the type has custom information about how to convert it's values
167
+ # into "data" - structures and values suitable for transportation and
168
+ # storage (JSON, etc.).
169
+ #
170
+ # If this method returns `true` then {#to_data} should succeed.
171
+ #
172
+ # @return [Boolean]
173
+ #
174
+ def has_to_data?
175
+ ! @to_data.nil?
176
+ end # #has_to_data?
177
+
178
+
179
+ # Dumps a value of this type to "data" - structures and values suitable
180
+ # for transport and storage, such as dumping to JSON or YAML, etc.
181
+ #
182
+ # @param [Object] value
183
+ # Value of this type (though it is *not* checked).
184
+ #
185
+ # @return [Object]
186
+ # The data representation of the value.
187
+ #
188
+ def to_data value
189
+ if @from_s.nil?
190
+ raise NoMethodError, "#to_data not defined"
191
+ end
192
+
193
+ @to_data.call value
194
+ end # #to_data
195
+
196
+
197
+ # @return [String]
198
+ # a brief string description of the type.
199
+ #
81
200
  def to_s
82
201
  "`Type: #{ name }`"
83
202
  end
203
+
84
204
  end # Type
85
205
  end # NRSER::Types
data/lib/nrser/types.rb CHANGED
@@ -41,6 +41,8 @@ using NRSER
41
41
 
42
42
  # Stuff to help you define, test, check and match types in Ruby.
43
43
  #
44
+ # {include:file:lib/nrser/types/README.md}
45
+ #
44
46
  module NRSER::Types
45
47
 
46
48
  # Make a {NRSER::Types::Type} from a value.
data/lib/nrser/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module NRSER
2
- VERSION = "0.0.24"
2
+ VERSION = "0.0.25"
3
3
 
4
4
  module Version
5
5
 
data/lib/nrser.rb CHANGED
@@ -1,4 +1,8 @@
1
- module NRSER; end
1
+ require 'pathname'
2
+
3
+ module NRSER
4
+ ROOT = ( Pathname.new(__FILE__).dirname / '..' ).expand_path
5
+ end
2
6
 
3
7
  require_relative './nrser/version'
4
8
  require_relative './nrser/no_arg'
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe "NRSER.bury!" do
4
+ subject { NRSER.method :bury! }
5
+
6
+ it do
7
+ expect(
8
+ {}.tap { |hash|
9
+ subject.call( hash, [:a, :b, :c], 1 )
10
+ }
11
+ ).to eq(
12
+ { a: { b: { c: 1 } } }
13
+ )
14
+ end
15
+
16
+ context "string key path" do
17
+ context ":parsed_key_type option omitted" do
18
+ it "creates hashes and sets string keys" do
19
+ expect(
20
+ {}.tap { |hash|
21
+ subject.call( hash, 'a.b.c', 1 )
22
+ }
23
+ ).to eq(
24
+ { 'a' => { 'b' => { 'c' => 1 } } }
25
+ )
26
+ end
27
+ end # :key_type option omitted
28
+
29
+ end # string key path
30
+
31
+ end # NRSER.bury
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe "NRSER.guess_name_type" do
4
+ subject { NRSER.method :guess_name_type }
5
+
6
+ it "can't guess about an empty hash" do
7
+ expect( subject.call( {} ) ).to be nil
8
+ end
9
+
10
+ it "guesses String when all keys are strings" do
11
+ expect( subject.call( {'a' => 1, 'b' => 2} ) ).to be String
12
+ end
13
+
14
+ it "guesses Symbol when all keys are symbols" do
15
+ expect( subject.call( {a: 1, b: 2} ) ).to be Symbol
16
+ end
17
+
18
+ it "guesses String when there are string keys but no symbols" do
19
+ expect(
20
+ subject.call({
21
+ 'a' => 1,
22
+ [:b] => 2,
23
+ 3 => 'three',
24
+ })
25
+ ).to be String
26
+ end
27
+
28
+ it "guesses Symbol when there are symbol keys but no strings" do
29
+ expect(
30
+ subject.call({
31
+ a: 1,
32
+ ['b'] => 2,
33
+ 3 => 'three',
34
+ })
35
+ ).to be Symbol
36
+ end
37
+
38
+ it "can't guess when there are string and symbol keys" do
39
+ expect(
40
+ subject.call({
41
+ a: 1,
42
+ 'b' => 2,
43
+ })
44
+ ).to be nil
45
+ end
46
+
47
+ end # NRSER.guess_name_type
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe "NRSER::Types.length" do
4
+ subject { NRSER::Types.method :length }
5
+
6
+ context "zero length" do
7
+ kwds = {
8
+ accepts: [ '', [], {}, ],
9
+ rejects: [ 'x', [1], {x: 1} ],
10
+ }
11
+
12
+ # Three ways to cut it:
13
+ it_behaves_like 'Type maker method', args: [ length: 0 ], **kwds
14
+ it_behaves_like 'Type maker method', args: [ 0 ], **kwds
15
+ it_behaves_like 'Type maker method', args: [ min: 0, max: 0], **kwds
16
+
17
+ end # zero length
18
+
19
+ it_behaves_like 'Type maker method',
20
+ args: [ {min: 3, max: 5}, name: '3to5Type' ],
21
+
22
+ accepts: [
23
+ [1, 2, 3],
24
+ [1, 2, 3, 4],
25
+ [1, 2, 3, 4, 5],
26
+ ],
27
+
28
+ rejects: [
29
+ [1, 2],
30
+ [1, 2, 3, 4, 5, 6]
31
+ ],
32
+
33
+ and_is_expected: {
34
+ to: {
35
+ have_attributes: {
36
+ name: '3to5Type',
37
+ }
38
+ }
39
+ }
40
+
41
+ end # NRSER::Types.length