Module: NRSER::Types

Extended by:
Factory
Includes:
Log::Mixin
Defined in:
lib/nrser/types.rb,
lib/nrser/types/in.rb,
lib/nrser/types/is.rb,
lib/nrser/types/nil.rb,
lib/nrser/types/not.rb,
lib/nrser/types/top.rb,
lib/nrser/types/is_a.rb,
lib/nrser/types/type.rb,
lib/nrser/types/when.rb,
lib/nrser/types/maybe.rb,
lib/nrser/types/pairs.rb,
lib/nrser/types/paths.rb,
lib/nrser/types/shape.rb,
lib/nrser/types/where.rb,
lib/nrser/types/arrays.rb,
lib/nrser/types/hashes.rb,
lib/nrser/types/labels.rb,
lib/nrser/types/tuples.rb,
lib/nrser/types/bounded.rb,
lib/nrser/types/factory.rb,
lib/nrser/types/numbers.rb,
lib/nrser/types/strings.rb,
lib/nrser/types/symbols.rb,
lib/nrser/types/booleans.rb,
lib/nrser/types/responds.rb,
lib/nrser/types/selector.rb,
lib/nrser/types/attributes.rb,
lib/nrser/types/eqiuvalent.rb,
lib/nrser/refinements/types.rb,
lib/nrser/types/collections.rb,
lib/nrser/types/combinators.rb

Overview

Stuff to help you define, test, check and match types in Ruby.

Read the documentation here.

Defined Under Namespace

Modules: Factory Classes: ArrayOfType, ArrayType, Attributes, Boolean, Bounded, CheckError, Combinator, Equivalent, False, FromStringError, HashOfType, HashType, Intersection, Is, IsA, Maybe, Not, Respond, Shape, Top, True, Tuple, Type, Union, When, Where, XOR

Constant Summary collapse

L_PAREN =

Constants

'('
R_PAREN =

‘❪’

')'
RESPONDS_WITH =

‘❫’

''
ASSOC =

‘->’

'=>'
LEQ =

terrible, don’t use: ‘⇒’

''
GEQ =
''
COMPLEXES =
''
REALS =
''
INTEGERS =
''
RATIONALS =
''
UNION =
''
AND =
'&'
NOT =

‘~’

'¬'
COMPLEMENT =
''
TILDE_PATH_RE =

Regular expression to match “tilde” user-home-relative paths.

Returns:

  • (Regexp)
/\A\~(?:\/|\z)/

In Type Factories collapse

Identity Equality Type Factories collapse

Nil Type Factories collapse

Negation Type Factories collapse

Class Instance Type Factories collapse

Type Factory Functions collapse

Pairs Type Factories collapse

Path Type Factories collapse

Shape Type Factories collapse

Array Type Factories collapse

Hash Type Factories collapse

Label Type Factories collapse

Bounded Type Factories collapse

Number Type Factories collapse

String Type Factories collapse

Symbol Type Factories collapse

Boolean Type Factories collapse

Method Response Type Factories collapse

Selector Type Factories collapse

Find Type Factories collapse

Attributes Type Factories collapse

Equivalent Type Factories collapse

Collection Type Factories collapse

Combinator Type Factories collapse

Class Method Summary collapse

Methods included from Factory

def_factory, def_type

Methods included from Log::Mixin

included, #logger, #logger=

Class Method Details

.AbsDirPath(**options) ⇒ Type

Absolute Path to a directory (both an AbsPath and an DirPath).

Parameters:

Returns:



214
215
216
217
218
219
220
# File 'lib/nrser/types/paths.rb', line 214

def_type :AbsDirPath,
&->( **options ) do
  self.Intersection \
    self.AbsPath,
    self.DirPath,
    **options
end

.AbsolutePath(**options) ⇒ Type

An absolute Path.

Parameters:

Returns:



115
116
117
118
119
120
121
122
123
124
125
# File 'lib/nrser/types/paths.rb', line 115

def_type        :AbsolutePath,
  aliases:      [ :AbsPath, :abs_path ],
  # TODO  IDK how I feel about this...
  # from_s:       ->( s ) { File.expand_path s },
&->( **options ) do
  self.Intersection \
    self.Path,
    # Weirdly, there is no {File.absolute?}..
    self.Attributes( to_pn: attrs( absolute?: true ) ),
    **options
end

.AbsPath(**options) ⇒ Type

A relative Path, which is just a Path that’s not AbsPath or TildePath.

Parameters:

Returns:



179
180
181
182
183
184
185
186
# File 'lib/nrser/types/paths.rb', line 179

def_type :RelPath,
&->( **options ) do
  self.Intersection \
    self.Path,
    !self.AbsPath,
    !self.TildePath,
    **options
end

.Array(item_type = self.Top, **options) ⇒ NRSER::Types::Type

TODO:

Make ‘list` into it’s own looser interface for “array-like” object API.

ArrayType / ArrayOfType factory function.

Parameters:

Returns:



205
206
207
208
209
210
211
212
213
214
# File 'lib/nrser/types/arrays.rb', line 205

def_type        :Array,
  parameterize: :item_type,
  aliases:    [ :list ],
&->( item_type = self.Top, **options ) do
  if item_type == self.Top
    ArrayType.new **options
  else
    ArrayOfType.new item_type, **options
  end
end

.ArrayPair(key: self.Top, value: self.Top, **options) ⇒ Type

Type for key/value pairs encoded as a Tuple (Array) of length 2.

Parameters:

  • key (TYPE) (defaults to: self.Top)

    Key type. Made into a type by make if it’s not already.

  • value (TYPE) (defaults to: self.Top)

    Value type. Made into a type by make if it’s not already.

  • options (Hash)

Returns:



45
46
47
48
49
50
51
52
53
# File 'lib/nrser/types/pairs.rb', line 45

def_type        :ArrayPair,
  default_name: false,
  parameterize: [ :key, :value ],
&->( key: self.Top, value: self.Top, **options ) do
  tuple \
    key,
    value,
    **options
end

.Attributes(attrs, **options) ⇒ Object

Get a Type that checks the types of one or more attributes on values.

Examples:

Type where first element of an Enumerable is a String

string_first = intersection Enumerable, attrs(first: String)

Parameters:



137
138
139
140
141
142
# File 'lib/nrser/types/attributes.rb', line 137

def_type          :Attributes,
  parameterize:   :attributes,
  aliases:      [ :attrs, ],
&->( attributes, **options ) do
  Attributes.new attributes, **options
end

.Bag(**options) ⇒ Type

An Enumerable that does not respond to ‘#each_pair`.

Meant to encompass Set, Array and the like without Hash and other associative containers.

Elements may or may not be indexed.

Parameters:

Returns:



83
84
85
86
87
88
89
90
# File 'lib/nrser/types/collections.rb', line 83

def_type        :Bag,
&->( **options ) do
  intersection \
    is_a( Enumerable ),
    self.not( respond_to( :each_pair ) ),
    name: name,
    **options
end

.Boolean(**options) ⇒ Type

True or False.

Parameters:

Returns:



146
147
148
149
150
# File 'lib/nrser/types/booleans.rb', line 146

def_type        :Boolean,
  aliases:    [ :bool ],
&->( **options ) do
  union self.True, self.False, **options
end

.Bounded(**options) ⇒ Type

Create a Bounded type instance that matches values between ‘min` and `max` (inclusive).

Parameters:

Returns:



103
104
105
106
107
# File 'lib/nrser/types/bounded.rb', line 103

def_type        :Bounded,
  parameterize: [ :min, :max ],
&->( min: nil, max: nil, **options ) do
  Bounded.new min: min, max: max, **options
end

.Character(encoding: nil, **options) ⇒ Type

String of length ‘1` (Ruby lacks a character class).

Parameters:

Returns:



142
143
144
145
146
# File 'lib/nrser/types/strings.rb', line 142

def_type        :Character,
  aliases:      [ :char ],
&->( encoding: nil, **options ) do
  self.String **options, length: 1, encoding: encoding
end

.check(value, type) ⇒ Object

Deprecated.

Old bang-less name for check!. We like out bangs around here.

Parameters:

  • value (*)

    Value to type check.

  • type (*)

    Type to check value against.

Returns:

  • The ‘value` parameter.

Raises:



129
130
131
132
133
134
135
# File 'lib/nrser/types.rb', line 129

def self.check value, type
  logger.deprecated \
    method: __method__,
    alternative: "NRSER::Types.check!"
  
  check! value, type
end

.check!(value, type) ⇒ Object

Create a Type from ‘type` with make and check that `value` satisfies it, raising if it doesn’t.

Parameters:

  • value (*)

    Value to type check.

  • type (*)

    Type to check value against.

Returns:

  • The ‘value` parameter.

Raises:



116
117
118
# File 'lib/nrser/types.rb', line 116

def self.check! value, type
  make( type ).check! value
end

.DirPath(**options) ⇒ Type

A Path that is a directory. Requires checking the file system.

Parameters:

Returns:



197
198
199
200
201
202
203
# File 'lib/nrser/types/paths.rb', line 197

def_type :DirPath,
&->( **options ) do
  self.Intersection \
    self.Path,
    self.Where( File.method :directory? ),
    **options
end

.EmptyString(encoding: nil, **options) ⇒ Type

Get a Type only satisfied by empty strings.

Parameters:

Returns:



108
109
110
111
112
# File 'lib/nrser/types/strings.rb', line 108

def_type        :EmptyString,
  aliases:      [ :empty_str ],
&->( encoding: nil, **options ) do
  self.String **options, length: 0, encoding: encoding
end

.EmptySymbol(**options) ⇒ Type

Exactly ‘:”`.

Pretty much just exists for use in NonEmptySymbol, which pretty much just exists for use in Label, which actually has some use ;)

Parameters:

Returns:



54
55
56
57
58
59
# File 'lib/nrser/types/symbols.rb', line 54

def_type        :EmptySymbol,
  aliases:      [ :empty_sym ],
  from_s:       :to_sym.to_proc,
&->( **options ) do
  self.Is :'', **options
end

.Equivalent(value, **options) ⇒ Type

Satisfied by values that ‘value` is `#==` to (`{ x : value == x }`).

Parameters:

Returns:

  • (Type)

    A type whose members are all instances of Ruby’s Numeric class.



77
78
79
80
81
82
# File 'lib/nrser/types/eqiuvalent.rb', line 77

def_type        :Equivalent,
  aliases:    [ :eq ],
  parameterize: :value,
&->( value, **options ) do
  Equivalent.new value, **options
end

.False(**options) ⇒ Type

A type whose only member is ‘false` and loads from common CLI and ENV var string representations (see False and NRSER::Types::False::STRINGS).

Parameters:

Returns:



132
133
134
135
# File 'lib/nrser/types/booleans.rb', line 132

def_type        :False,
&->( **options ) do
  False.new **options
end

.FilePath(**options) ⇒ Object

A Path that is a file (using File.file? to test).



231
232
233
234
235
236
237
# File 'lib/nrser/types/paths.rb', line 231

def_type :FilePath,
&->( **options ) do
  self.Intersection \
    self.Path,
    self.Where( File.method :file? ),
    **options
end

.from_repr(repr) ⇒ Object

make a type instance from a object representation that can come from a YAML or JSON declaration.



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/nrser/types.rb', line 239

def self.from_repr repr
  match repr, {
    str => ->(string) {
      NRSER::Types.method(string.downcase).call
    },
    
    Hash => ->(hash) {
      raise NotImplementedError, "Haven't gotten to it yet!"
    },
  }
end

.Has(member, **options) ⇒ Type

TODO:

The “find” factories got introduced to support Selector, and need improvement. They’re really just stop gaps at the moment, and have already been considerably changed a few times.

I want to eventually make selectors able to output SQL, MongoDB, etc. queries, which will require we get rid of the Where usage…

Type that tests value for membership in a group object via that object’s ‘#include?` method.

Parameters:

Returns:



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/nrser/types/selector.rb', line 172

def_type        :Has,
  parameterize: :member,
  aliases:      [ :has, :includes ],
&->( member, **options ) do
  # Provide a some-what useful default name
  options[:name] ||= "Has<#{ NRSER.smart_ellipsis member.inspect, 64 }>"
  
  member_type = make member

  where( **options ) { |value|
    value.respond_to?( :find ) &&
      # value.find { |entry| member_type === entry }
      value.find( &member_type )
  }
end

.HasAny(*members, **options) ⇒ Type

TODO:

The “find” factories got introduced to support Selector, and need improvement. They’re really just stop gaps at the moment, and have already been considerably changed a few times.

I want to eventually make selectors able to output SQL, MongoDB, etc. queries, which will require we get rid of the Where usage…

Match values that have any of ‘members`.

Parameters:

  • members (Array<TYPE>)

    Resulting type will be satisfied by values in which it can ‘#find` any entry that any of `members` is satisfied by. `members` entries that are not Type instances will be made into them via make.

  • options (Hash)

Returns:



213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/nrser/types/selector.rb', line 213

def_type        :HasAny,
  parameterize: :members,
  aliases:      [ :intersects ],
&->( *members, **options ) do
  options[:name] ||= \
    "HasAny<#{ NRSER.smart_ellipsis members.inspect, 64 }>"

  member_types = members.map { |m| make m }
  
  where( **options ) {
    |group| member_types.any? { |member_type| group.find &member_type }
  }
end

.Hash(keys: self.Top, values: self.Top, **options) ⇒ HashType

Type satisfied by Hash instances with optional key and value types.

Parameters:

  • keys (TYPE) (defaults to: self.Top)

    Type for the hash keys. Will be made into a type by make if it’s not one already.

    WARNING Don’t pass ‘nil` unless you mean that all the keys must be

    `nil`! Omit the keyword or pass {.Top}.
    
  • values (TYPE) (defaults to: self.Top)

    Type for the hash values. Will be made into a type by make if it’s not one already.

    WARNING Don’t pass ‘nil` unless you mean that all the values must be

    `nil`! Omit the keyword or pass {.Top}.
    
  • options (Hash)

Returns:



238
239
240
241
242
243
244
245
246
247
# File 'lib/nrser/types/hashes.rb', line 238

def_type        :Hash,
  aliases:      [ :dict, :hash_type ],
  parameterize: [ :keys, :values ],
&->( keys: self.Top, values: self.Top, **options ) do
  if keys != self.Top || values != self.Top
    HashOfType.new keys: keys, values: values, **options
  else
    HashType.new **options
  end
end

.HashPair(key: self.Top, value: self.Top, **options) ⇒ Type

Type whose members are single a key/value pairs encoded as Hash instances with a single entry (‘::Hash#length==1`).

Parameters:

  • key (TYPE) (defaults to: self.Top)

    Key type. Made into a type by make if it’s not already.

  • value (TYPE) (defaults to: self.Top)

    Value type. Made into a type by make if it’s not already.

  • options (Hash)

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/nrser/types/pairs.rb', line 72

def_type        :HashPair,
  default_name: false,
  parameterize: [ :key, :value ],
&->( key: self.Top, value: self.Top, **options ) do
  key = self.make key
  value = self.make value

  options[:name] ||= "Hash<(#{ key.name }, #{ value.name })>"
  
  options[:symbolic] ||= "(#{ key.symbolic }=>#{ value.symbolic })"
  
  intersection \
    self.Hash( keys: key, values: value ),
    self.Length( 1 ),
    **options
end

.HomePath(**options) ⇒ Type

A path that starts with ‘~`, meaning it’s relative to a user’s home directory (to Ruby, see note below).

> ### Note: How Bash and Ruby Think Differently About Home Paths ### > > #### Ruby Always Tries to Go Home #### > > From my understanding and fiddling around Ruby considers any path that > starts with ‘~` a “home path” for the purpose of expanding, such as in > File.expand_path and Pathname#expand_path. > > You can see this clearly in the [rb_file_expand_path_internal][] C > function, which is where those expand methods end up. > > [rb_file_expand_path_internal]: github.com/ruby/ruby/blob/61bef8612afae25b912627e69699ddbef81adf93/file.c#L3486 > > #### Bash #### > > However > > However - Bash 4’s ‘cd` - on MacOSX, at least - treats `~some_user` as > being a home directory *only if* `some_user` exists… and you may have > a file or directory in the working dir named `~some_user` that it will > correctly fall back on if `some_user` does not exist. > > Paths are complicated, man.

Parameters:

Returns:



161
162
163
164
165
166
167
# File 'lib/nrser/types/paths.rb', line 161

def_type        :HomePath,
&->( **options ) do
  self.Intersection \
    self.Path,
    self.Respond( to: [ :start_with?, '~' ], with: self.True ),
    **options
end

.In(group, **options) ⇒ Type

TODO:

I think I want to get rid of where… which would elevate this to it’s own class as a “fundamental” concept (I guess)… not so sure, really. The idea of membership is pretty wide-spread and important, but it’s a bit a vague and inconsistently implemented things.

Type that tests value for membership in a group object via that object’s ‘#include?` method.

Parameters:

Returns:

Raises:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/nrser/types/in.rb', line 46

def_type        :In,
  aliases:      [ :member_of ],
  from_s:       ->( s ) { s },
  default_name: ->( group, **options ) {
                  "In<#{ NRSER.smart_ellipsis group.inspect, 64 }>"
                },
  parameterize: :group,
&->( group, **options ) do
  unless group.respond_to? :include?
    raise NRSER::TypeError,
      "In `group` must respond to `:include?`",
      group: group
  end

  # TODO  This is a step in the right direction (from anon {Proc}) but I
  #       now think what we really want is 
  #       
  #           where group, :include?
  #       
  self.Where group.method( :include? ), **options
end

.Integer(**options) ⇒ Type

Instances of the built-in Integer class.

Parameters:

Returns:



83
84
85
86
87
88
89
90
91
# File 'lib/nrser/types/numbers.rb', line 83

def_type      :Integer,
  symbolic:   '',
  from_s:     method( :parse_number ),
  aliases:  [ :int,
              :integer,
              :signed ],
&->( **options ) do
  IsA.new Integer, **options
end

.Intersection(*types, **options) ⇒ Type

Match all of the types

Parameters:

Returns:



280
281
282
283
284
285
# File 'lib/nrser/types/combinators.rb', line 280

def_type        :Intersection,
  aliases:      [ :all_of, :and ],
  parameterize: [ :types ],
&->( *types, **options ) do
  Intersection.new *types, **options
end

.Is(**options) ⇒ Type

Satisfied by the exact value only (identity comparison via ‘#equal?`).

Useful for things like Module, Class, Fixnum, Symbol, ‘true`, etc.

Parameters:

Returns:



71
72
73
74
75
# File 'lib/nrser/types/is.rb', line 71

def_type        :Is,
  parameterize: :value,
&->( value, **options ) do
  Is.new value, **options
end

.IsA(module_, **options) ⇒ Type

Create a type

{ x : x.is_a?( mod ) == true }

If ‘mod` is a Class, the returned Type will be satisfied by instances of `mod`.

If ‘mod` is a non-Class Module, the returned Type will be satisfied by instances of classes that include `mod`.

Parameters:

Returns:



124
125
126
127
128
# File 'lib/nrser/types/is_a.rb', line 124

def_type        :IsA,
  parameterize: :mod,
&->( module_, **options ) do
  IsA.new module_, **options
end

.Label(**options) ⇒ Type

A label is a non-empty String or Symbol.

Parameters:

Returns:



35
36
37
38
39
40
41
# File 'lib/nrser/types/labels.rb', line 35

def_type        :Label,
&->( **options ) do
  self.Union \
    self.NonEmptyString,
    self.NonEmptySymbol,
    **options
end

.length(exact, options = {}) ⇒ NRSER::Types::Attributes .length(bounds, options = {}) ⇒ NRSER::Types::Attributes

Overloads:

  • .length(exact, options = {}) ⇒ NRSER::Types::Attributes

    Get a length attribute type that specifies an ‘exact` value.

    Examples:

    only_type = NRSER::Types.length 1
    
    only_type.test []
    # => false
    
    only_type.test [:x]
    # => true
    
    only_type.test [:x, :y]
    # => false

    Parameters:

    • exact (Integer)

      Exact non-negative integer that the length must be to satisfy the type created.

    • options (Hash) (defaults to: {})

      Options hash passed up to Type constructor.

    Returns:

  • .length(bounds, options = {}) ⇒ NRSER::Types::Attributes

    Get a length attribute type satisfied by values within a ‘:min` and `:max` (inclusive).

    Examples:

    three_to_five = NRSER::Types.length( {min: 3, max: 5}, name: '3-5' )
    three_to_five.test [1, 2]               # => false
    three_to_five.test [1, 2, 3]            # => true
    three_to_five.test [1, 2, 3, 4]         # => true
    three_to_five.test [1, 2, 3, 4, 5]      # => true
    three_to_five.test [1, 2, 3, 4, 5, 6]   # => false

    Parameters:

    • bounds (Hash)
    • options (Hash) (defaults to: {})

      Options hash passed up to Type constructor.

    Options Hash (bounds):

    • :min (Integer)

      An optional minimum value that the ‘#length` should not be less than.

    • :max (Integer)

      An optional maximum value that the ‘#length` should not be more than.

    • :length (Integer)

      An optional value for both the minimum and maximum.

    Returns:



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/nrser/types/attributes.rb', line 203

def_type        :Length,
  # TODO  This would need special attention if we ever started using the
  #       `parameterize` data for anything...
  parameterize: :args,
&->( *args ) do
  bounds = {}
  options = if args[1].is_a?( Hash ) then args[1] else {} end
  
  case args[0]
  when ::Integer
    # It's just a length
    return attrs(
      { length: is( non_neg_int.check!( args[0] ) ) },
      **options
    )
    
    bounds[:min] = bounds[:max] = non_neg_int.check args[0]
    
  when ::Hash
    # It's keyword args
    kwds = args[0].sym_keys
    
    # Pull any :min and :max in the keywords
    bounds[:min] = kwds.delete :min
    bounds[:max] = kwds.delete :max
    
    # But override with :length if we got it
    if length = kwds.delete(:length)
      bounds[:min] = length
      bounds[:max] = length
    end
    
    # (Reverse) merge anything else into the options (options hash values
    # take precedence)
    options = kwds.merge options
    
  else
    raise ArgumentError, <<-END.squish
      arg must be positive integer or option hash, found:
      #{ args[0].inspect } of type #{ args[0].class }
    END
    
  end
  
  bounded_type = self.Bounded bounds
  
  length_type = if !bounded_type.min.nil? && bounded_type.min >= 0
    # We don't need the non-neg check
    bounded_type
  else
    # We do need the non-neg check
    intersection(non_neg_int, bounded_type)
  end
  
  options[:name] ||= "Length<#{ bounded_type.name }>"
  
  self.Attributes({ length: length_type }, options)
end

.make(value) ⇒ NRSER::Types::Type

Make a Type from a value.

If the ‘value` argument is…

  • a Type, it is returned.

  • a Class, a new IsA matching that class is returned.

    This allows things like

    NRSER::Types.check 's', String
    NRSER::Types.match 's', String, ->(s) { ... }
    
  • anything else, a new Is matching that value is returned.

Parameters:

Returns:



81
82
83
84
85
86
87
88
89
# File 'lib/nrser/types.rb', line 81

def self.make value
  if value.nil?
    self.Nil
  elsif value.is_a? NRSER::Types::Type
    value
  else
    self.When value
  end
end

.makerMethod

The make method reference; for easy map and such.

Returns:



96
97
98
# File 'lib/nrser/types.rb', line 96

def self.maker
  method :make
end

.Map(**options) ⇒ Type

A “hash-like” Enumerable that responds to ‘#each_pair` and `#[]`.

Parameters:

Returns:



58
59
60
61
62
63
64
65
66
67
# File 'lib/nrser/types/collections.rb', line 58

def_type        :Map,
  aliases:      [ :hash_like, :assoc ],
&->( **options ) do
  intersection \
    is_a( Enumerable ),
    respond_to( :each_pair ),
    respond_to( :[] ),
    name: name,
    **options
end

.match(value, *clauses) ⇒ Object

TODO:

Doc this crap.

My own shitty version of pattern matching!

Raises:



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/nrser/types.rb', line 179

def self.match value, *clauses
  if clauses.empty?
    raise ArgumentError.new NRSER.dedent <<-END
      Must supply either a single {type => expression} hash argument or a
      even amount of arguments representing (type, expression) pairs after
      `value`.
      
      #{ NRSER::Version.doc_url 'NRSER/Types#match-class_method' }
    END
  end
  
  enum = if clauses.length == 1 && clauses.first.respond_to?(:each_pair)
    clauses.first.each_pair
  else
    unless clauses.length % 2 == 0
      raise TypeError.new NRSER.dedent <<-END
        When passing a list of clauses, it must be an even length
        representing (type, expression) pairs.
        
        Found an argument list with length #{ clauses.length }:
        
        #{ clauses }
      END
    end
    
    clauses.each_slice(2)
  end
  
  enum.each { |type, expression|
    if test? value, type
      # OK, we matched! Is the corresponding expression callable?
      if expression.respond_to? :call
        # It is; invoke and return result.
        if expression.arity == 0
          return expression.call
        else
          return expression.call value
        end
      else
        # It's not; assume it's a value and return it.
        return expression
      end
    end
  }
  
  raise TypeError, <<-END.dedent
    Could not match value
    
        #{ value.inspect }
    
    to any of types
    
        #{ enum.map {|type, expression| "\n    #{ type.inspect }"}.join '' }
    
  END
end

.Maybe(type, **options) ⇒ Type

Type satisfied by ‘nil` or the parametrized type.

Parameters:

  • type (Type)

    The type values must be if they are not ‘nil`.

  • options (Hash)

    (see Type.initialize)

Returns:



80
81
82
83
84
85
# File 'lib/nrser/types/maybe.rb', line 80

def_type        :Maybe,
  parameterize: :type,
  default_name: false,
&->( type, **options ) do
  Maybe.new type, **options
end

.NegativeInteger(**options) ⇒ Type

Integer less than zero.

Parameters:

Returns:



125
126
127
128
129
130
131
132
133
134
# File 'lib/nrser/types/numbers.rb', line 125

def_type      :NegativeInteger,
  symbolic:   'ℤ⁻',
  aliases:  [ :neg_int,
              :negative_int ],
&->( **options ) do
  intersection \
    self.Integer,
    self.Bounded( max: -1 ),
    **options
end

.Nil(**options) ⇒ Type

TODO:

Should we have a ‘#from_s` that converts the empty string to `nil`?

Kind-of seems like we would want that to be a different types so that you can have a Nil type that is distinct from the empty string in parsing, but also have a type that accepts the empty string and coverts it to ‘nil`?

Something like:

type = t.empty | t.non_empty_str
type.from_s ''
# => nil
type.from_s 'blah'
# => 'blah'

Type for ‘nil`; itself and only.

Parameters:

Returns:



50
51
52
53
54
55
56
# File 'lib/nrser/types/nil.rb', line 50

def_type        :Nil,
  aliases:      [ :null ],
  # `.Nil?` would not make any sense...
  maybe:        false,
&->( **options ) do
  is nil, **options
end

.NonEmptyPathname(**options) ⇒ Type

A Pathname that isn’t empty. Because not emptiness is often important.

Parameters:

Returns:



62
63
64
65
66
67
68
# File 'lib/nrser/types/paths.rb', line 62

def_type        :NonEmptyPathname,
&->( **options ) do
  self.Intersection \
    self.Pathname,
    self.Attributes( to_s: self.NonEmptyString ),
    **options
end

.NonEmptyString(encoding: nil, **options) ⇒ Type

String of length ‘1` or more.

Parameters:

Returns:



125
126
127
128
129
# File 'lib/nrser/types/strings.rb', line 125

def_type        :NonEmptyString,
  aliases:      [ :non_empty_str ],
&->( encoding: nil, **options ) do
  self.String  **options, length: {min: 1}, encoding: encoding
end

.NonEmptySymbol(**options) ⇒ Type

A Symbol that is not ‘:”`.

Parameters:

Returns:



70
71
72
73
74
75
76
77
# File 'lib/nrser/types/symbols.rb', line 70

def_type        :NonEmptySymbol,
  aliases:      [ :non_empty_sym ],
&->( **options ) do
  self.Intersection \
    self.Symbol,
    ~self.EmptySymbol,
    **options
end

.NonNegativeInteger(**options) ⇒ Type

Positive integers and zero… but it seems more efficient to define these as bounded instead of a union.

Parameters:

Returns:



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/nrser/types/numbers.rb', line 146

def_type      :NonNegativeInteger,
  symbolic:   'ℕ⁰',
  aliases:  [ :non_neg_int,
              :unsigned,
              :index,
              :non_negative_int, ],
&->( **options ) do
  intersection \
    self.Integer,
    self.Bounded( min: 0 ),
    **options
end

.NonPositiveInteger(**options) ⇒ Type

Negative integers and zero.

Parameters:

Returns:



168
169
170
171
172
173
174
175
176
177
178
# File 'lib/nrser/types/numbers.rb', line 168

def_type      :NonPositiveInteger,
  symbolic:   '{0}∪ℤ⁻',
  aliases:  [ :non_pos_int,
              :non_positive_int,
              :non_positive_integer ],
&->( **options ) do
  intersection \
    self.Integer,
    self.Bounded( max: 0 ),
    **options
end

.NormalizedPath(**options) ⇒ Type

TODO:

Document NormalizedPath type factory.

Parameters:

Returns:



248
249
250
251
252
253
254
255
# File 'lib/nrser/types/paths.rb', line 248

def_type        :NormalizedPath,
  aliases:      [ :NormPath, :norm_path ],
&->( **options ) do
  self.Intersection \
    self.Path,
    self.Where( NRSER.method :normalized_path? ),
    **options
end

.Not(type, **options) ⇒ Not

Negates another type.

Parameters:

Returns:



87
88
89
90
91
92
# File 'lib/nrser/types/not.rb', line 87

def_type        :Not,
  default_name: false,
  parameterize: :type,
&->( type, **options ) do
  Not.new type, **options
end

.Numeric(**options) ⇒ Type

The Ruby Numeric type, which is the super-class of all number classes: Integer, Float, Rational, Complex.

In set theory notation this would either be expressed as either:

  1. ℤ ∪ ℚ ∪ ℝ ∪ ℂ

depending on how you want to thing about the embeddability of the sets within each other (ℤ is embeddable in ℚ, which is embeddable in ℝ, which is embeddable in ℂ).

However, I feel like (2) is not at all useful for expressing the type, and I feel like the default of just using the NRSER::Types::Type#name as the NRSER::Types::Type#symbolic is easier to read than (1), so this type does not provide a ‘symbolic:` keyword argument.

Parameters:

Returns:

  • (Type)

    A type whose members are all instances of Ruby’s Numeric class.



60
61
62
63
64
65
66
67
68
69
# File 'lib/nrser/types/numbers.rb', line 60

def_type    :Numeric,
  aliases:  [ :num, :number, :numeric ],
  # symbolic: [ INTEGERS,
  #             RATIONALS,
  #             REALS,
  #             COMPLEXES ].join( " #{ UNION } " ),
  from_s:   method( :parse_number ),
&->( **options ) do
  IsA.new Numeric, **options
end

.Pair(key: self.Top, value: self.Top, **options) ⇒ Type

A key/value pair, which can be encoded as an Array of length 2 or a Hash of length 1.

Parameters:

  • key (TYPE) (defaults to: self.Top)

    Key type. Made into a type by make if it’s not already.

  • value (TYPE) (defaults to: self.Top)

    Value type. Made into a type by make if it’s not already.

  • options (Hash)

Returns:



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/nrser/types/pairs.rb', line 105

def_type        :Pair,
  default_name: false,
  parameterize: [ :key, :value ],
&->( key: self.Top, value: self.Top, **options ) do
  key = self.make key
  value = self.make value

  options[:name] ||= if key == self.Top && value == self.Top
    "Pair"
  else
    "Pair<#{ key.name }, #{ value.name }>"
  end

  options[:symbolic] ||= "(#{ key.symbolic }, #{ value.symbolic })"
  
  union \
    self.ArrayPair( key: key, value: value ),
    self.HashPair(  key: key, value: value ),
    **options
end

.parse_number(string) ⇒ Integer, Float

Parse a string into a number.

Returns:

  • (Integer)

    If the string represents a whole integer.

  • (Float)

    If the string represents a decimal number.



26
27
28
29
30
# File 'lib/nrser/types/numbers.rb', line 26

def self.parse_number string
  float = Float string
  int = float.to_i
  if float == int then int else float end
end

.Path(**options) ⇒ Type

Parameters:

Returns:



79
80
81
82
83
84
85
# File 'lib/nrser/types/paths.rb', line 79

def_type        :Path,
&->( **options ) do
  self.Union \
    self.NonEmptyString,
    self.NonEmptyPathname,
    **options
end

.Pathname(**options) ⇒ Type

Just a type for instances of Pathname.

Parameters:

Returns:



46
47
48
49
50
51
# File 'lib/nrser/types/paths.rb', line 46

def_type    :Pathname,
  to_data:  :to_s,
  from_s:   ->( string ) { Pathname.new string } \
do |**options|
  is_a Pathname, **options
end

.PositiveInteger(**options) ⇒ Type

Integers greater than zero.

Parameters:

Returns:



105
106
107
108
109
110
111
112
113
114
# File 'lib/nrser/types/numbers.rb', line 105

def_type      :PositiveInteger,
  symbolic: 'ℤ⁺',
  aliases:  [ :pos_int,
              :positive_int ],
&->( **options ) do
  intersection \
    self.Integer,
    bounded( min: 1 ),
    **options
end

.POSIXPathSegment(**options) ⇒ Type

A POSIX path segment (directory, file name) - any NonEmptyString that doesn’t have ‘/` in it.

Parameters:

Returns:



97
98
99
100
101
102
103
104
# File 'lib/nrser/types/paths.rb', line 97

def_type        :POSIXPathSegment,
  aliases:      [ :path_segment, :path_seg ],
&->( **options ) do
  self.Intersection \
    self.NonEmptyString,
    self.Respond( to: [:include?, '/'], with: false ),
    **options
end

.Respond(to: , with: , publicly: true, **options) ⇒ Respond

Create a Respond type.

Returns:



140
141
142
143
144
145
# File 'lib/nrser/types/responds.rb', line 140

def_type        :Respond,
  default_name: false,
  parameterize: [ :to, :with, :publicly ],
&->( to:, with:, publicly: true, **options ) do
  Respond.new to: to, with: with, publicly: publicly, **options
end

.RespondTo(method_name, **options) ⇒ Respond

Gets a Respond that admits values that ‘#respond_to?` a `method_name`.

Parameters:

Returns:



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/nrser/types/responds.rb', line 159

def_type        :RespondTo,
  default_name: ->( method_name, **options ) {
                  "RespondTo<#{ method_name }>"
                },
  parameterize: :method_name,
  # TODO  I'm not sure how this worked before, but defining `.respond_to?` 
  #       def fucks things up...
  # maybe:        false,
&->( method_name, **options ) do
  respond to: [:respond_to?, method_name], with: self.True
end

.Selector(pairs, **options) ⇒ Shape

Factory to create Shape type instances that function as MongoDB-esque document query against lists of Ruby objects using the standard Enumerable#select and related methods.

Selectors are in the very early and experimental stage, but it’s something I’ve been thinking about for a while now that suddenly just sort-of fell into place.

Eventually I want to be able to use these same selectors on SQL, MongoDB, ActiveRecord, etc.

Examples:

# Some sample data

people = [
  {name: "Neil", fav_color: "blue", likes: ["cat", "scotch", "computer"]},
  {name: "Mica", fav_color: "red", like: ["cat", "beer", "dance"]},
]

# Simple value matching

people.select( &t[ name: "Neil" ] ).map &[:name]
# => [ "Neil" ]

# NOTE  1.  We're using the `t -> NRSER::Types` short-hand alias, as 
#           provided by `using NRSER::Types` refinement.
#           
#       2.  {NRSER::Types.[]} is a short-hand alias for 
#           {NRSER::Types.Selector}.
#           
#       3.  The `&[:name]` uses NRSER's {Array#to_proc} core extension.
#           It's equivalent to `{ |h| h[:name] }`.
#       

people.select( &t[ fav_color: "red" ] ).map &[:name]
# => [ "Mica" ]

# Fields that are {NRSER::Types.Bag} (think Array and Set but not Hash)
# match against *any* of the values

people.select( &t[ likes: "cat" ] ).map &[:name]
# => [ "Neil", "Mica" ]

# Literal arrays are treated like literals, however, and must match
# *exactly*

people.select( &t[ likes: [ "cat", "computer" ] ] ).map &[:name]
# => []

people.select( &t[ likes: [ "cat", "computer", "scotch" ] ] ).map &[:name]
# => []

people.select( &t[ likes: [ "cat", "scotch", "computer" ] ] ).map &[:name]
# => ["Neil"]

# To match against *any* of a list of terms you can construct a 
# {NRSER::Types.HasAny}

people.select( &t[ likes: t.HasAny( "computer", "dance" ) ]).map &[:name]
# => ["Neil", "Mica"]

# The {NRSER::Types.Has} and {.HasAny} do create types for the terms and
# do a `#find` against them, so you can use any of the {NRSER::Types}
# system in there.
# 
# Here is using a {RegExp} (which {NRSER::Types.make} wraps in a
# {NRSER::Types::When}):

people.select( &t[ fav_color: /\A[bg]/ ] ).map &[:name]

Parameters:

Returns:

See Also:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/nrser/types/selector.rb', line 119

def_type        :Selector,
  aliases:      [ :query, :[] ],
  # I don't think we need the `?` methods for Selector?
  maybe:        false,
  parameterize: :pairs,
&->( pairs, **options ) do
  shape \
    pairs.transform_values { |value|
      if value.is_a?( Type )
        value
      else
        value_type = self.When value
        self.or(
          value_type,
          (bag & has( value_type )),
          name: "{#{ value.inspect  }}"
        )
      end
    },
    **options
end

.Shape(pairs, **options) ⇒ Shape

Create a Shape type that parameterizes ‘pairs` of object keys and Type values.

Members of the type are values ‘v` for which for all keys `k` and paired value types `t_k` `v` is a member of `t_k`:

shape.pairs.all? { |k, t_k| t_k.test? v[k] }

Parameters:

Returns:



137
138
139
140
141
# File 'lib/nrser/types/shape.rb', line 137

def_type          :Shape,
  parameterize:   true,
&->( pairs, **options ) do
  Shape.new pairs, **options
end

.String(length: nil, encoding: nil, **options) ⇒ Type

Get a Type whose members IsA String, along with some other optional common attribute checks (String#length and String#encoding).

If ‘encoding:` is specified and no `from_s:` is provided, will add a Type#form_s that attempts to transcode strings that are not already in the target encoding (via a simple `String#encode( encoding )`).

If you for some reason don’t want NRSER::Types::Type#from_s to try to transcode, just provide a ‘from_s:` Proc that doesn’t do it - ‘->( s ) { s }` to just use whatever tha cat drags in.

If ‘from_s` is otherwise not provided, adds the obvious identity function.

Parameters:

  • length (nil | Integer | {min: Integer?, max: Integer?, length: Integer?}) (defaults to: nil)

    Optionally admit only strings of a specified length. This does not affect any default ‘from_s` - loaded strings must already be the specific length.

  • encoding (String) (defaults to: nil)

    Optional String#encoding check. See notes above regarding default ‘from_s` that may be added.

  • options (Hash)

Returns:



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/nrser/types/strings.rb', line 58

def_type        :String,
  aliases:      [ :str ],
&->( length: nil, encoding: nil, **options ) do

  if [ length, encoding ].all?( &:nil? )
    # Give 'er the obvious `#from_s` if she don't already have one
    options[:from_s] ||= ->( s ) { s }

    IsA.new ::String, **options
    
  else
    types = [ IsA.new( ::String ) ]
    types << self.Length( length ) if length

    if encoding
      # If we didn't get a `from_s`, provide one that will try to transcode to
      # `encoding` (unless it's already there)
      options[:from_s] ||= ->( string ) {
        if string.encoding == encoding
          string
        else
          string.encode encoding
        end
      }

      types << self.Attributes( encoding: encoding )

    else
      # We don't need to handle encoding, so set the obvious `#from_s` if 
      # one was not provided
      options[:from_s] ||= ->( s ) { s }

    end
    
    self.Intersection *types, **options
  end

end

.Symbol(**options) ⇒ Type

Symbol instances. Load from strings as you would expect String#to_sym.

Parameters:

Returns:



35
36
37
38
39
40
# File 'lib/nrser/types/symbols.rb', line 35

def_type        :Symbol,
  aliases:      [ :sym ],
  from_s:       :to_sym.to_proc,
&->( **options ) do
  self.IsA ::Symbol, **options
end

.test(value, type) ⇒ Boolean

Old question-less name for test?. We like our marks around here.

Parameters:

  • value (Object)

    Value to test for membership.

  • type (TYPE)

    Type to see if value satisfies. Passed through make to make sure it’s a Type first.

Returns:

  • (Boolean)

    ‘true` if `value` satisfies `type`.



162
163
164
165
166
167
168
# File 'lib/nrser/types.rb', line 162

def self.test value, type
  logger.deprecated \
    method: __method__,
    alternative: "NRSER::Types.test?"
  
  test? value, type
end

.test?(value, type) ⇒ Boolean

Create a Type from ‘type` with make and test if `value` satisfies it.

Parameters:

  • value (Object)

    Value to test for membership.

  • type (TYPE)

    Type to see if value satisfies. Passed through make to make sure it’s a Type first.

Returns:

  • (Boolean)

    ‘true` if `value` satisfies `type`.



151
152
153
# File 'lib/nrser/types.rb', line 151

def self.test? value, type
  make(type).test value
end

.Tree(**options) ⇒ Type

Either a Vector or Map - Enumerable collections with indexed elements that work with the NRSER “tree” functions.

Parameters:

Returns:



102
103
104
105
106
107
108
109
# File 'lib/nrser/types/collections.rb', line 102

def_type        :Tree,
&->( **options ) do
  union \
    array_like,
    hash_like,
    name: name,
    **options
end

.True(**options) ⇒ Type

A type whose only member is ‘true` and loads from common CLI and ENV var string representations (see True and NRSER::Types::True::STRINGS).

Parameters:

Returns:



117
118
119
120
# File 'lib/nrser/types/booleans.rb', line 117

def_type  :True,
&->( **options ) do
  True.new **options
end

.Tuple(*types, **options) ⇒ Tuple

Get a Tuple type.

Parameters:

Returns:



141
142
143
144
145
146
# File 'lib/nrser/types/tuples.rb', line 141

def_type        :Tuple,
  default_name: false,
  parameterize: :types,
&->( *types, **options ) do
  Tuple.new *types, **options
end

.Type(**options) ⇒ IsA

TODO:

This is where the methods that load types from data and strings that are used by CLI apps to spec params and the like should go!

A type whose members are Type instances themselves.

Parameters:

Returns:



143
144
145
146
# File 'lib/nrser/types/is_a.rb', line 143

def_type        :Type,
&->( **options ) do
  IsA NRSER::Types::Type, **options
end

.Union(*types, **options) ⇒ Type

Match any of the types.

Parameters:

Returns:



260
261
262
263
264
265
# File 'lib/nrser/types/combinators.rb', line 260

def_type        :Union,
  aliases:      [ :one_of, :or ],
  parameterize: [ :types ],
&->( *types, **options ) do
  Union.new *types, **options
end

.UNIXPort(**options) ⇒ Type

A valid UNIX port number Integer, which is a 16-bit unsigned integer that can ot be ‘0`.

Parameters:

Returns:



213
214
215
216
217
218
219
220
# File 'lib/nrser/types/numbers.rb', line 213

def_type      :UNIXPort,
  aliases:  [ :port, ],
&->( **options ) do
  intersection \
    self.Integer,
    self.Bounded( min: 1, max: (2**16 - 1) ),
    **options
end

.Unsigned16BitInteger(**options) ⇒ Type

Unsigned 16-bit Integer type.

Parameters:

Returns:



192
193
194
195
196
197
198
199
200
201
# File 'lib/nrser/types/numbers.rb', line 192

def_type      :Unsigned16BitInteger,
  symbolic:   'uint16',
  aliases:  [ :uint16,
              :ushort ],
&->( **options ) do
  intersection \
    self.Integer,
    self.Bounded( min: 0, max: ((2 ** 16) - 1) ),
    **options
end

.UTF8Character(**options) ⇒ Type

A type satisfied by UTF-8 encoded Character.

Parameters:

Returns:



181
182
183
184
185
186
187
188
# File 'lib/nrser/types/strings.rb', line 181

def_type        :UTF8Character,
  # NOTE        "UTF8Character".underscore -> "utf8_character"
  aliases:      [ :utf_8_character,
                  :utf8_char,
                  :utf_8_char ],
&->( **options ) do
  self.Character **options, encoding: Encoding::UTF_8
end

.UTF8String(length: nil, **options) ⇒ Type

A type satisfied by UTF-8 encoded String.

Parameters:

Returns:



159
160
161
162
163
164
165
166
167
168
# File 'lib/nrser/types/strings.rb', line 159

def_type        :UTF8String,
  # NOTE        "UTF8String".underscore -> "utf8_string"
  aliases:      [ :utf_8_string,
                  :utf8,
                  :utf_8,
                  :utf8_str,
                  :utf_8_str ],
&->( length: nil, **options ) do
  self.String **options, length: length, encoding: Encoding::UTF_8
end

.Vector(**options) ⇒ Type

An “array-like” Enumerable that responds to ‘#each_index` and `#slice` / `#[]`.

Parameters:

Returns:



37
38
39
40
41
42
43
44
45
46
47
# File 'lib/nrser/types/collections.rb', line 37

def_type        :Vector,
  aliases:      [ :array_like ],
&->( **options ) do
  intersection \
    is_a( Enumerable ),
    respond_to( :each_index ),
    respond_to( :slice ),
    respond_to( :[] ),
    name: name,
    **options
end

.When(value, **options) ⇒ When

Get a type parameterizing a ‘value` whose members are all objects `obj` such that `value === obj` (“case equality”).

Parameters:

Returns:



137
138
139
140
141
142
# File 'lib/nrser/types/when.rb', line 137

def_type          :When,
  parameterize:   :value,
  default_name:   false,
&->( value, **options ) do
  When.new value, **options
end

.XOR(*types, **options) ⇒ Type

Match one of the types only.

Parameters:

Returns:



300
301
302
303
304
305
# File 'lib/nrser/types/combinators.rb', line 300

def_type        :XOR,
  aliases:      [ :exclusive_or, :only_one_of ],
  parameterize: [ :types ],
&->( *types, **options ) do
  XOR.new *types, **options
end