Class: NRSER::Types::Type

Inherits:
Object show all
Defined in:
lib/nrser/types/type.rb

Direct Known Subclasses

AnyType, AttrsType, Bounded, Combinator, Is, IsA, Maybe, Not, Respond, Shape, When, Where

Display Instance Methods collapse

Validation Instance Methods collapse

Loading Values Instance Methods collapse

Dumping Values Instance Methods collapse

Language Integration Instance Methods collapse

Derivation Instance Methods collapse

Instance Method Summary collapse

Constructor Details

#initialize(name: nil, from_s: nil, to_data: nil, from_data: nil) ⇒ Type

Instantiate a new ‘NRSER::Types::Type`.

Parameters:

  • name: (nil | String) (defaults to: nil)

    Name that will be used when displaying the type, or ‘nil` to use a default generated name.

  • from_s: (nil | #call) (defaults to: nil)

    Callable that will be passed a String and should return an object that satisfies the type if it possible to create one.

    The returned value will be checked against the type, so returning a value that doesn’t satisfy will result in a NRSER::TypeError being raised by #from_s.

  • to_data: (nil | #call | #to_proc) (defaults to: nil)

    Optional callable (or object that responds to ‘#to_proc` so we can get a callable) to call to turn type members into “data”.



48
49
50
51
52
53
54
55
56
57
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
# File 'lib/nrser/types/type.rb', line 48

def initialize name: nil, from_s: nil, to_data: nil, from_data: nil
  @name = name
  @from_s = from_s
  
  @to_data = if to_data.nil?
    nil
  elsif to_data.respond_to?( :call )
    to_data
  elsif to_data.respond_to?( :to_proc )
    to_data.to_proc
  else
    raise TypeError.new binding.erb <<-ERB
      `to_data:` keyword arg must be `nil`, respond to `#call` or respond
      to `#to_proc`.
      
      Found value:
      
          <%= to_data.pretty_inspect %>
      
      (type <%= to_data.class %>)
      
    ERB
  end
  
  @from_data = if from_data.nil?
    nil
  elsif from_data.respond_to?( :call )
    from_data
  elsif from_data.respond_to?( :to_proc )
    from_data.to_proc
  else
    raise TypeError.new binding.erb <<-ERB
      `to_data:` keyword arg must be `nil`, respond to `#call` or respond
      to `#to_proc`.
      
      Found value:
      
          <%= from_data.pretty_inspect %>
      
      (type <%= from_data.class %>)
      
    ERB
  end
end

Instance Method Details

#===(value) ⇒ Boolean

Hook into Ruby’s *case subsumption* operator to allow usage in ‘case` statements! Forwards to #test?.

Parameters:

  • value (Object)

    Value to test for type satisfaction.

Returns:

  • (Boolean)

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



398
399
400
# File 'lib/nrser/types/type.rb', line 398

def === value
  test? value
end

#builtin_inspectObject

Inspecting

Due to their combinatoric nature, types can quickly become large data hierarchies, and the built-in #inspect will produce a massive dump that’s distracting and hard to decipher.

#inspect is readily used in tools like ‘pry` and `rspec`, significantly impacting their usefulness when working with types.

As a solution, we alias the built-in ‘#inspect` as #builtin_inspect, so it’s available in situations where you really want all those gory details, and point #inspect to #explain.



453
# File 'lib/nrser/types/type.rb', line 453

alias_method :builtin_inspect, :inspect

#check(*args, &block) ⇒ Object

Old name for #check! without the bang.



201
# File 'lib/nrser/types/type.rb', line 201

def check *args, &block; check! *args, &block; end

#check!(value, &details) ⇒ Object

Check that a ‘value` satisfies the type.

Returns:

  • (Object)

    The value itself.

Raises:

See Also:



190
191
192
193
194
195
196
197
198
# File 'lib/nrser/types/type.rb', line 190

def check! value, &details
  # success case
  return value if test? value
  
  raise NRSER::Types::CheckError.new \
    value: value,
    type: self,
    details: details
end

#explainString

A string that gives our best concise description of the type’s logic, in particular exposing any composite types that it’s made up of.

Used as the #name when a custom one is not provided.

Meant for inline display, so the result *should not* contain newlines.

Realizing subclasses should override this method, as this implementation only returns the class’ name (and just the last segment, for brevity’s sake).

Examples:

Base implementation is not very interesting

MyType = Class.new NRSER::Types::Type
my_type = MyType.new
my_type.explain
# => "MyType"

Returns:



139
140
141
# File 'lib/nrser/types/type.rb', line 139

def explain
  self.class.demod_name
end

#from_data(data) ⇒ Object

Try to load a value from “data” - basic values and collections like Array and Hash forming tree-like structures.

Parameters:

  • data (*)

    Data to try to load from.

Raises:



329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/nrser/types/type.rb', line 329

def from_data data
  unless has_from_data?
    raise NoMethodError, "#from_data not defined"
  end
  
  value = if @from_data
    @from_data.call data
  else
    custom_from_data data
  end
  
  check! value
end

#from_s(string) ⇒ Object

Load a value of this type from a string representation by passing ‘string` to the @from_s Proc.

Checks the value @from_s returns with #check! before returning it, so you know it satisfies this type.

Realizing classes **should not** need to override this - they can define a ‘#custom_from_s` instance method for it to use, allowing individual types to still override that by providing a `from_s:` proc keyword arg at construction. This also lets them avoid checking the returned value, since we do so here.

Parameters:

  • string (String)

    String representation.

Returns:

Raises:

  • (NoMethodError)

    If this type doesn’t know how to load values from strings.

    In basic types this happens when #initialize was not provided a ‘from_s:` Proc argument.

    NRSER::Types::Type subclasses may override #from_s entirely, divorcing it from the ‘from_s:` constructor argument and internal @from_s instance variable (which is why @from_s is not publicly exposed - it should not be assumed to dictate #from_s behavior in general).

  • (TypeError)

    If the value loaded does not pass #check.



287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/nrser/types/type.rb', line 287

def from_s string
  unless has_from_s?
    raise NoMethodError, "#from_s not defined for type #{ name }"
  end
  
  value = if @from_s
    @from_s.call string
  else
    custom_from_s string
  end
  
  check! value
end

#has_from_data?Boolean

Test if the type can load values from “data” - basic values and collections like Array and Hash forming tree-like structures.

Realizing classes may need to override this to limited or expand responses relative to parameterized types.

Returns:

  • (Boolean)


310
311
312
313
314
# File 'lib/nrser/types/type.rb', line 310

def has_from_data?
  !@from_data.nil? ||
    # Need the `true` second arg to include protected methods
    respond_to?( :custom_from_data, true )
end

#has_from_s?Boolean

Note:

When this method returns ‘true` it simply indicates that some method of loading from strings exists - the load itself can of course still fail.

Test if the type knows how to load values from strings.

Looks for the ‘@from_s` instance variable or a `#custom_from_s` method.

Realizing classes should only need to override this method to limited or expand the scope relative to parameterized types.

Returns:

  • (Boolean)


247
248
249
250
251
# File 'lib/nrser/types/type.rb', line 247

def has_from_s?
  !@from_s.nil? ||
    # Need the `true` second arg to include protected methods
    respond_to?( :custom_from_s, true )
end

#has_to_data?Boolean

Test if the type has custom information about how to convert it’s values into “data” - structures and values suitable for transportation and storage (JSON, etc.).

If this method returns ‘true` then #to_data should succeed.

Returns:

  • (Boolean)


357
358
359
# File 'lib/nrser/types/type.rb', line 357

def has_to_data?
  ! @to_data.nil?
end

#inspectObject



454
455
456
457
458
459
460
461
462
463
# File 'lib/nrser/types/type.rb', line 454

def inspect
  name = self.name
  explain = self.explain
  
  if name == explain
    explain
  else
    "#{ name } := #{ explain }"
  end
end

#intersection(*others) ⇒ NRSER::Types::Intersection Also known as: &, and

Return an intersection type satisfied by values that satisfy both ‘self` and all of `others`.

Parameters:

Returns:



500
501
502
503
504
# File 'lib/nrser/types/type.rb', line 500

def intersection *others
  require_relative './combinators'
  
  NRSER::Types.intersection self, *others
end

#nameString

What this type likes to be called (and displayed as by default).

Custom names can be provided when constructing most types via the ‘name:` keyword, which allows thinking about composite and complicated types in simpler and application-specific terms.

Realizing subclasses **should not** override this method - they should pass a ‘name:` keyword up to #initialize, which sets the `@name` instance variable that is then used here.

If no name is provided to #initialize, this method will fall back to #explain.

Returns:



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

def name
  @name || explain
end

#notNRSER::Types::Not Also known as: ~

Return a “negation” type satisfied by all values that do not satisfy ‘self`.

Returns:



532
533
534
535
536
# File 'lib/nrser/types/type.rb', line 532

def not
  require_relative './not'
  
  NRSER::Types.not self
end

#respond_to?(name, include_all = false) ⇒ Boolean

Overridden to customize behavior for the #from_s, #from_data and #to_data methods - those methods are always defined, but we have #respond_to? return ‘false` if they lack the underlying instance variables needed to execute.

Examples:

t1 = t.where { |value| true }
t1.respond_to? :from_s
# => false

t2 = t.where( from_s: ->(s){ s.split ',' } ) { |value| true }
t2.respond_to? :from_s
# => true

Parameters:

  • name (Symbol | String)

    Method name to ask about.

  • include_all (Boolean) (defaults to: false)

    IDK, part of Ruby API that is passed up to ‘super`.

Returns:

  • (Boolean)


425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/nrser/types/type.rb', line 425

def respond_to? name, include_all = false
  case name.to_sym
  when :from_s
    has_from_s?
  when :from_data
    has_from_data?
  when :to_data
    has_to_data?
  else
    super name, include_all
  end
end

#test(value) ⇒ Boolean

Deprecated.

Old name for #test?.

Parameters:

  • value (Object)

    Value to test for type satisfaction.

Returns:

  • (Boolean)

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

Raises:



177
# File 'lib/nrser/types/type.rb', line 177

def test value; test? value; end

#test?(value) ⇒ Boolean

See if a value satisfies the type.

Realizing classes must implement this method.

This implementation just defines the API; it always raises AbstractMethodError.

Parameters:

  • value (Object)

    Value to test for type satisfaction.

Returns:

  • (Boolean)

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

Raises:



165
166
167
# File 'lib/nrser/types/type.rb', line 165

def test? value
  raise NRSER::AbstractMethodError.new( self, __method__ )
end

#to_data(value) ⇒ Object

Dumps a value of this type to “data” - structures and values suitable for transport and storage, such as dumping to JSON or YAML, etc.

Parameters:

  • value (Object)

    Value of this type (though it is not checked).

Returns:

  • (Object)

    The data representation of the value.



371
372
373
374
375
376
377
# File 'lib/nrser/types/type.rb', line 371

def to_data value
  if @to_data.nil?
    raise NoMethodError, "#to_data not defined"
  end
  
  @to_data.call value
end

#to_sString

Proxies to #name.

Returns:



389
# File 'lib/nrser/types/type.rb', line 389

def to_s; name; end

#union(*others) ⇒ NRSER::Types::Union Also known as: |, or

Return a union type satisfied by values that satisfy either ‘self` or and of `others`.

Parameters:

Returns:



482
483
484
485
486
# File 'lib/nrser/types/type.rb', line 482

def union *others
  require_relative './combinators'
  
  NRSER::Types.union self, *others
end

#xor(*others) ⇒ NRSER::Types::Intersection Also known as: ^

Return an *exclusive or* type satisfied by values that satisfy either ‘self` or `other` *but not both*.

Parameters:

Returns:



518
519
520
521
522
# File 'lib/nrser/types/type.rb', line 518

def xor *others
  require_relative './combinators'
  
  NRSER::Types.xor self, *others
end