Class: NRSER::Types::Type

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

Overview

Definitions

Direct Known Subclasses

Attributes, Bounded, Combinator, Equivalent, Is, IsA, Maybe, Not, Respond, Shape, Top, 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, symbolic: nil, from_s: nil, to_data: nil, from_data: nil) ⇒ Type

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

Parameters:

  • name (nil | #to_s) (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 NRSER::Types.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”.



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
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/nrser/types/type.rb', line 54

def initialize  name: nil,
                symbolic: nil,
                from_s: nil,
                to_data: nil,
                from_data: nil
  @name = name.nil? ? nil : name.to_s
  @from_s = from_s
  @symbolic = symbolic
  
  @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.



506
507
508
# File 'lib/nrser/types/type.rb', line 506

def === value
  test? value
end

#builtin_inspectString

The built-in ‘#inspect` method, aliased before we override it so it’s available here if you should want it.

See #inspect for rationale.

Returns:



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

alias_method :builtin_inspect, :inspect

#check(*args, &block) ⇒ Object

Old name for #check! without the bang.



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

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:



305
306
307
308
309
310
311
312
313
# File 'lib/nrser/types/type.rb', line 305

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

#default_namenil, String

Optional support for generated names to use when no name is explicitly provided at initialization instead of falling back to #explain.

Realizing subclasses SHOULD override this method IF they are able to generate meaningful names.

The #default_name implementation returns ‘nil` indicating it is not able to generate names.

Returns:

  • (nil)

    When a name can not be generated.

  • (String)

    The generated name.



130
131
132
# File 'lib/nrser/types/type.rb', line 130

def default_name
  nil
end

#default_symbolicnil, String

Optional support for generated symbolic names to use when no symbolic name is provided at initialization instead of falling back to #name.

Realizing subclasses SHOULD override this method IF they are able to generate meaningful symbolic names.

The #default_symbolic implementation returns ‘nil` indicating it is not able to generate names.

Returns:

  • (nil)

    When a symbolic name can not be generated.

  • (String)

    The generated symbolic name.



162
163
164
# File 'lib/nrser/types/type.rb', line 162

def default_symbolic
  nil
end

#explainString

A verbose breakdown of the implementation internals of the type.

Realizing classes SHOULD override this method with a precise implementation.

The #explain implementation dumps all instance variables that appear to have accessor methods and whose names to not start with ‘_`.

Check out the display table for examples.

Returns:



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
# File 'lib/nrser/types/type.rb', line 192

def explain
  @_explain ||= begin
    s = "#{ self.class.demod_name }<"

    ivars = instance_variables.
      # each_with_object( {} ) { |var_name, hash|
      each_with_object( [] ) { |var_name, array|
        method_name = var_name.to_s[1..-1]

        if  method_name.start_with?( '_' ) ||
            !respond_to?( method_name )
          next
        end

        value = instance_variable_get var_name

        unless value.nil?
          array << "#{ var_name }=#{ value.inspect }"
        end
      }.join( ', ' )
    
    s += " #{ ivars }" unless ivars.empty?
    s += '>'
    s
  end
end

#from_data(data) ⇒ Object

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

Parameters:

  • data (Object)

    Data to try to load from.

Raises:



444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/nrser/types/type.rb', line 444

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.



402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/nrser/types/type.rb', line 402

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 NRSER::Types.Array and NRSER::Types.Hash forming tree-like structures.

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

Returns:



425
426
427
428
429
# File 'lib/nrser/types/type.rb', line 425

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:



362
363
364
365
366
# File 'lib/nrser/types/type.rb', line 362

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:



472
473
474
# File 'lib/nrser/types/type.rb', line 472

def has_to_data?
  ! @to_data.nil?
end

#inspectString

Overridden to point to #to_s.

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 #to_s.

Returns:



254
255
256
# File 'lib/nrser/types/type.rb', line 254

def inspect
  to_s
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:



597
598
599
600
601
# File 'lib/nrser/types/type.rb', line 597

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

#nameString

The name is the type as you would write it out in documentation.

Prefers an explicit ‘@name` set at initialization, falling back first to #default_name, then to #explain.

Returns:



142
143
144
# File 'lib/nrser/types/type.rb', line 142

def name
  @name || default_name || explain
end

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

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

Returns:



629
630
631
632
633
# File 'lib/nrser/types/type.rb', line 629

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:



533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/nrser/types/type.rb', line 533

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

#symbolicString

A set theory-esque symbolic representation of the type, if available.

Prefers an explicit ‘@symbolic` set at initialization, falling back to an explicit `@name`, then to #default_symbolic and finally #name.

Returns:



174
175
176
# File 'lib/nrser/types/type.rb', line 174

def symbolic
  @symbolic || @name || default_symbolic || name
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:



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

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:



280
281
282
# File 'lib/nrser/types/type.rb', line 280

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.



486
487
488
489
490
491
492
# File 'lib/nrser/types/type.rb', line 486

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

#to_procProc<(Object)=>Boolean>

This small method is extremely useful - it lets you easily use types to in Enumerable#select and similar by returning a Proc that runs #test? on the values it receives.

Pretty cool, right?

Examples:

Select all non-empty strings and positive integers from an enum

enum.select &(non_empty_str | pos_int)

Returns:



558
559
560
# File 'lib/nrser/types/type.rb', line 558

def to_proc
  ->( value ) { test? value }
end

#to_sString

How to display the type. Proxies to #symbolic.

Returns:



224
225
226
# File 'lib/nrser/types/type.rb', line 224

def to_s
  symbolic
end

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

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

Parameters:

Returns:



579
580
581
582
583
# File 'lib/nrser/types/type.rb', line 579

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:



615
616
617
618
619
# File 'lib/nrser/types/type.rb', line 615

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