Class: NRSER::Props::Prop

Inherits:
Object show all
Includes:
Log::Mixin
Defined in:
lib/nrser/props/prop.rb

Overview

‘Prop` instances hold the configuration for a property defined on propertied classes.

Props are immutable by design.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Log::Mixin

included, #logger, #logger=

Constructor Details

#initialize(defined_in, name, type: t.any, default: nil, source: nil, to_data: nil, from_data: nil, index: nil, reader: nil, writer: nil, aliases: []) ⇒ Prop

Instantiate a new ‘Prop` instance.

You should not need to construct a ‘Prop` directly unless you are doing custom meta-programming - they should be constructed for you via the `.prop` “macro” defined at NRSER::Props::Props::ClassMethods#prop that is extended in to classes including NRSER::Props::Props.

Parameters:

  • default (nil | Proc | Object) (defaults to: nil)

    A default value or a Proc used to get default values for primary props. Dervied props (those that have a #source) may not

    At least one of ‘default:` and `source:` must be `nil`.

  • source (nil | Symbol | String | Proc) (defaults to: nil)

    Source that provides the prop’s value. See details for how each type is handled in #source. Strings are converted to symbols.

    At least one of ‘default:` and `source:` must be `nil`.

Raises:

  • (ArgumentError)

    If ‘default:` is not `nil` and `source:` is not `nil`.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/nrser/props/prop.rb', line 140

def initialize  defined_in,
                name,
                type: t.any,
                default: nil,
                source: nil,
                to_data: nil,
                from_data: nil,
                index: nil,
                reader: nil,
                writer: nil,
                aliases: []
  
  # Set these up first so {#to_s} works in case we need to raise errors.
  @defined_in = defined_in
  @name = t.sym.check name
  @index = t.non_neg_int?.check! index
  @type = t.make type
  
  # Will be overridden in {#init_default!} if needed
  @deps = []
  
  @to_data = to_data
  @from_data = from_data
  
  @reader, @writer = [ reader, writer ].map do |value|
    t.match value,
      t.bool?,
        value,
        
      t.hash_( keys: t.sym, values: t.bool ),
        :freeze.to_proc,
        
      t.hash_( keys: t.label, values: t.bool ),
        ->( hash ) { hash.map { |k, v| [ k.to_sym, v ] }.to_h.freeze }
  end
  
  @aliases = t.array( t.sym ).check! aliases
  
  # Source
  
  # normalize source to {nil}, {Symbol} or {Proc}
  @source = t.match source,
    nil,        nil,
    String,     ->( string ) { string.to_sym },
    Symbol,     source,
    Proc,       ->(){ source }
  
  # Detect if the source points to an instance variable (`:'@name'`-formatted
  # symbol).
  @instance_variable_source = \
    @source.is_a?( Symbol ) && @source.to_s[0] == '@'
  
  init_default! default
  
end

Instance Attribute Details

#aliasesArray<Symbol> (readonly)

TODO document ‘aliases` attribute.

Returns:



110
111
112
# File 'lib/nrser/props/prop.rb', line 110

def aliases
  @aliases
end

#defined_inClass (readonly)

The class the prop was defined in.

Returns:

  • (Class)


51
52
53
# File 'lib/nrser/props/prop.rb', line 51

def defined_in
  @defined_in
end

#depsArray<Symbol> (readonly)

Names of any props this one depends on (to create a default).

Returns:



80
81
82
# File 'lib/nrser/props/prop.rb', line 80

def deps
  @deps
end

#indexnil | Integer (readonly)

The key under which the value will be stored in the storage.

Returns:

  • (nil | Integer)


66
67
68
# File 'lib/nrser/props/prop.rb', line 66

def index
  @index
end

#nameSymbol (readonly)

The name of the prop, which is used as it’s method name and key where applicable.

Returns:



59
60
61
# File 'lib/nrser/props/prop.rb', line 59

def name
  @name
end

#sourcenil, ... (readonly)

Optional name of instance variable (including the ‘@` prefix) or getter method (method that takes no arguments) that provides the property’s value.

Props that have a source are considered derived, those that don’t are called primary.

Returns:

  • (nil)

    When this prop is a primary property and receives it’s value at initialization or from a #default.

  • (Symbol)

    This prop is derived by returning an instance variable if the symbol starts with ‘@` or otherwise by sending the symbol to the prop’d instance (calling that method with no arguments).

  • (Proc)

    This prop is derived by evaluating this Proc in the prop’d instance.



103
104
105
# File 'lib/nrser/props/prop.rb', line 103

def source
  @source
end

#typeNRSER::Types::Type (readonly)

The type of the valid values for the property.

Returns:



73
74
75
# File 'lib/nrser/props/prop.rb', line 73

def type
  @type
end

Instance Method Details

#check!(value) ⇒ VALUE

Check that a value satisfies the #type, raising if it doesn’t.

Parameters:

  • value (VALUE)

    Value to check.

Returns:

  • (VALUE)

    ‘value` arg that was passed in.

Raises:



555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# File 'lib/nrser/props/prop.rb', line 555

def check! value
  type.check!( value ) do
    binding.erb <<-END
      Value of type <%= value.class.safe_name %> for prop <%= self.full_name %>
      failed type check.
      
      Must satisfy type:
      
          <%= type %>
      
      Given value:
      
          <%= value.pretty_inspect %>
      
    END
  end
end

#create_reader?(name) ⇒ Boolean

Used by the NRSER::Props::Props::ClassMethods.prop “macro” method to determine if it should create a reader method on the propertied class.

Parameters:

  • name (Symbol)

    The prop name or alias in question.

Returns:

  • (Boolean)

    ‘true` if a reader method should be created for the prop value.



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/nrser/props/prop.rb', line 315

def create_reader? name
  t.sym.check! name
  
  # If the options was explicitly provided then use that
  case @reader
  when nil
    # Fall through
  when Hash
    return @reader[name] if @reader.key? name
    # else fall through
  when true, false
    return @reader
  end
      
  # return @reader unless @reader.nil?
  
  # Always create readers for primary props
  return true if primary?
  
  # Don't override methods
  return false if defined_in.instance_methods.include?( name )
  
  # Create if {#source} is a {Proc} so it's accessible
  return true if Proc === source
  
  # Source is a symbol; only create if it's not the same as the name
  return source != name
end

#create_writer?(name) ⇒ Boolean

Used by the NRSER::Props::Props::ClassMethods.prop “macro” method to determine if it should create a writer method on the propertied class.

Right now, we don’t create writers, but we will probably make them an option in the future, which is why this stub is here.

Returns:

  • (Boolean)

    Always ‘false` for the moment.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/nrser/props/prop.rb', line 354

def create_writer? name
  # If the options was explicitly provided then use that
  case @writer
  when nil
    # Fall through
  when Hash
    return @writer[name] if @writer.key? name
    # else fall through
  when true, false
    return @writer
  end
  
  storage_immutable = defined_in..storage.try( :immutable? )
  
  return !storage_immutable unless storage_immutable.nil?
  
  false
end

#default(**values) ⇒ Object

TODO:

This is a shitty hack stop-gap until I really figure our how this should work.

Get the default value for the property given the instance and values.

Parameters:

  • values (Hash<Symbol, Object>)

    Other prop values known at this point, keyed by name.

Returns:

Raises:

  • (NameError)

    If the prop doesn’t have a default.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/nrser/props/prop.rb', line 420

def default **values
  if has_default?
    if Proc === @default
      case @default.arity
      when 0
        @default.call
      else
        kwds = values.slice *deps
        @default.call **kwds
      end
    else
      @default
    end
  else
    raise NameError.new binding.erb <<-END
      Prop <%= full_name %> has no default value (and none provided).
    END
  end
end

#defined_in_nameObject



374
375
376
377
# File 'lib/nrser/props/prop.rb', line 374

def defined_in_name
  return defined_in.safe_name if defined_in.respond_to?( :safe_name )
  defined_in.to_s
end

#full_nameString

Full name with class prop was defined in.

Examples:

MyMod::SomeClass.props[:that_prop].full_name
# => 'MyMod::SomeClass#full_name'

Returns:



388
389
390
# File 'lib/nrser/props/prop.rb', line 388

def full_name
  "#{ defined_in_name }##{ name }"
end

#get(instance) ⇒ Object

Note:

At the moment, values are not type-checked when reading. Primary values are checked when setting, so they should be ok, but this does leave the possibility that props with a #source may return values that do not satisfy their types… :/

Get the value of the prop from an instance.

Parameters:

Returns:

  • Value from the instance’s prop value storage (for #primary? props) or it’s #source.



486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/nrser/props/prop.rb', line 486

def get instance
  if source?
    if instance_variable_source?
      instance.instance_variable_get source
    else
      t.match source,
        t.or( String, Symbol ),
          ->( name ) { instance.send name },
        Proc,
          ->( block ) { instance.instance_exec &block }
    end
  else
    # Go through the storage engine
    instance.class..storage.get instance, self
  end
end

#has_default?Boolean Also known as: default?

Test if this prop is configured to provide default values.

Returns:

  • (Boolean)


397
398
399
# File 'lib/nrser/props/prop.rb', line 397

def has_default?
  @has_default
end

#instance_variable_source?Boolean

Is the value of this prop coming from an instance variable?

Returns:

  • (Boolean)


454
455
456
# File 'lib/nrser/props/prop.rb', line 454

def instance_variable_source?
  @instance_variable_source
end

#namesObject

Instance Methods



301
302
303
# File 'lib/nrser/props/prop.rb', line 301

def names
  [name, *aliases]
end

#primary?Boolean

Does this property represent and independent value (not sourced)?

Primary properties are important because they’re the ones you need to store and transport to reconstruct the instance.

Returns:

  • (Boolean)


466
467
468
# File 'lib/nrser/props/prop.rb', line 466

def primary?
  !source?
end

#set(instance, value) ⇒ nil

Set a value for a the prop on an instance.

Parameters:

Returns:

  • (nil)

Raises:



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/nrser/props/prop.rb', line 519

def set instance, value
  unless primary?
    raise RuntimeError.new binding.erb <<~END
      Only {#primary?} props can be set!
      
      Tried to set prop #{ prop.name } to value
      
          <%= value.pretty_inspect %>
      
      in instance
      
          <%= instance.pretty_inspect %>
      
    END
  end
  
  instance.class..storage.put \
    instance,
    self,
    check!( value )
  
  nil
end

#source?Boolean

Does this property have a source method that it gets it’s value from?

Returns:

  • (Boolean)


445
446
447
# File 'lib/nrser/props/prop.rb', line 445

def source?
  !@source.nil?
end

#to_data(instance) ⇒ Object

Get the “data” value - a basic scalar or structure of hashes, arrays and scalars, suitable for JSON encoding, etc. - for the property from an instance.

The process depends on the ‘to_data:` keyword provided at property declaration:

  1. nil default

    • If the property value responds to ‘#to_data`, the result of invoking that method will be returned.

      WARNING

      This can cause infinite recursion if an instance has a property value that is also an instance of the same class (as as other more complicated scenarios that boil down to the same problem), but, really, what else would it do in this situation?

      This problem can be avoided by by providing a ‘to_data:` keyword when declaring the property that dictates how to handle it’s value. In fact, that was the motivation for adding ‘to_data:`.

    • Otherwise, the value itself is returned, under the assumption that it is already suitable as data.

  2. Symbol | String

    • The ‘to_data:` string or symbol is sent to the property value (the method with this name is called via Object#send).

  3. Proc

    • The ‘to_data:` proc is called with the property value as the sole argument and the result is returned as the data.

Parameters:

  • instance (NRSER::Props::Props)

    Instance to get the property value form.

Returns:

  • (Object)

    Data representation of the property value (hopefully - the value itself is returned if we don’t have any better options, see above).

Raises:

  • (TypeError)

    If @to_data (provided via the ‘to_data:` keyword at property declaration) is anything other than nil, String, Symbol or Proc.



618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'lib/nrser/props/prop.rb', line 618

def to_data instance
  value = get instance
  
  case @to_data
  when nil
    if value.respond_to? :to_data
      value.to_data
    elsif type.respond_to? :to_data
      type.to_data value
    else
      value
    end
  when Symbol, String
    value.send @to_data
  when Proc
    @to_data.call value
  else
    raise TypeError.squished <<-END
      Expected `@to_data` to be Symbol, String or Proc;
      found #{ @to_data.inspect }
    END
  end
end

#to_descObject



702
703
704
# File 'lib/nrser/props/prop.rb', line 702

def to_desc
  "#{ full_name }:#{ type }"
end

#to_sString

Returns a short string describing the instance.

Returns:

  • (String)

    a short string describing the instance.



710
711
712
# File 'lib/nrser/props/prop.rb', line 710

def to_s
  "#<#{ self.class.safe_name } #{ to_desc }>"
end

#value_from_data(data) ⇒ VALUE

Load a value for the prop from “data”.

Parameters:

  • data (*)

Returns:

  • (VALUE)

    The prop value to use.



650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
# File 'lib/nrser/props/prop.rb', line 650

def value_from_data data
  value = case @from_data
  when nil
    # This {Prop} does not have any custom `from_data` instructions, which
    # means we must rely on the {#type} to covert *data* to a *value*.
    # 
    if data.is_a?( String ) && type.has_from_s?
      type.from_s data
    elsif type.has_from_data?
      type.from_data data
    else
      data
    end
    
  when Symbol, String
    # The custom `from_data` configuration specifies a string or symbol name,
    # which we interpret as a class method on the defining class and call
    # with the data to produce a value.
    @defined_in.send @from_data, data
  
  when Proc
    # The custom `from_data` configuration provides a procedure, which we
    # call with the data to produce the value.
    @from_data.call data
    
  else
    raise TypeError.new binding.erb <<-ERB
      Expected `@from_data` to be Symbol, String or Proc;
      found <%= @from_data.class %>.
      
      Acceptable types:
      
      -   Symbol or String
          -   Name of class method on the class this property is defined in
              (<%= @defined_in %>) to call with data to convert it to a
              property value.
              
      -   Proc
          -   Procedure to call with data to convert it to a property value.
      
      Found `@from_data`:
      
          <%= @from_data.pretty_inspect %>
      
      (type <%= @from_data.class %>)
      
    ERB
  end
  
end