nrser 0.0.24 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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