Class: NRSER::Props::Prop
- 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
-
#aliases ⇒ Array<Symbol>
readonly
TODO document ‘aliases` attribute.
-
#defined_in ⇒ Class
readonly
The class the prop was defined in.
-
#deps ⇒ Array<Symbol>
readonly
Names of any props this one depends on (to create a default).
-
#index ⇒ nil | Integer
readonly
The key under which the value will be stored in the storage.
-
#name ⇒ Symbol
readonly
The name of the prop, which is used as it’s method name and key where applicable.
-
#source ⇒ nil, ...
readonly
Optional name of instance variable (including the ‘@` prefix) or getter method (method that takes no arguments) that provides the property’s value.
-
#type ⇒ NRSER::Types::Type
readonly
The type of the valid values for the property.
Instance Method Summary collapse
-
#check!(value) ⇒ VALUE
Check that a value satisfies the #type, raising if it doesn’t.
-
#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.
-
#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.
-
#default(**values) ⇒ Object
Get the default value for the property given the instance and values.
- #defined_in_name ⇒ Object
-
#full_name ⇒ String
Full name with class prop was defined in.
-
#get(instance) ⇒ Object
Get the value of the prop from an instance.
-
#has_default? ⇒ Boolean
(also: #default?)
Test if this prop is configured to provide default values.
-
#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
constructor
Instantiate a new ‘Prop` instance.
-
#instance_variable_source? ⇒ Boolean
Is the value of this prop coming from an instance variable?.
-
#names ⇒ Object
Instance Methods ============================================================================.
-
#primary? ⇒ Boolean
Does this property represent and independent value (not sourced)?.
-
#set(instance, value) ⇒ nil
Set a value for a the prop on an instance.
-
#source? ⇒ Boolean
Does this property have a source method that it gets it’s value from?.
-
#to_data(instance) ⇒ Object
Get the “data” value - a basic scalar or structure of hashes, arrays and scalars, suitable for JSON encoding, etc.
- #to_desc ⇒ Object
-
#to_s ⇒ String
A short string describing the instance.
-
#value_from_data(data) ⇒ VALUE
Load a value for the prop from “data”.
Methods included from Log::Mixin
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.
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
#aliases ⇒ Array<Symbol> (readonly)
TODO document ‘aliases` attribute.
110 111 112 |
# File 'lib/nrser/props/prop.rb', line 110 def aliases @aliases end |
#defined_in ⇒ Class (readonly)
The class the prop was defined in.
51 52 53 |
# File 'lib/nrser/props/prop.rb', line 51 def defined_in @defined_in end |
#deps ⇒ Array<Symbol> (readonly)
Names of any props this one depends on (to create a default).
80 81 82 |
# File 'lib/nrser/props/prop.rb', line 80 def deps @deps end |
#index ⇒ nil | Integer (readonly)
The key under which the value will be stored in the storage.
66 67 68 |
# File 'lib/nrser/props/prop.rb', line 66 def index @index end |
#name ⇒ Symbol (readonly)
The name of the prop, which is used as it’s method name and key where applicable.
59 60 61 |
# File 'lib/nrser/props/prop.rb', line 59 def name @name end |
#source ⇒ nil, ... (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.
103 104 105 |
# File 'lib/nrser/props/prop.rb', line 103 def source @source end |
#type ⇒ NRSER::Types::Type (readonly)
The type of the valid values for the property.
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.
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.
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.
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
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.
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_name ⇒ Object
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_name ⇒ String
Full name with class prop was defined in.
388 389 390 |
# File 'lib/nrser/props/prop.rb', line 388 def full_name "#{ defined_in_name }##{ name }" end |
#get(instance) ⇒ Object
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.
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.
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?
454 455 456 |
# File 'lib/nrser/props/prop.rb', line 454 def instance_variable_source? @instance_variable_source end |
#names ⇒ Object
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.
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.
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?
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:
-
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.
-
-
-
The ‘to_data:` string or symbol is sent to the property value (the method with this name is called via Object#send).
-
-
Proc
-
The ‘to_data:` proc is called with the property value as the sole argument and the result is returned as the data.
-
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_desc ⇒ Object
702 703 704 |
# File 'lib/nrser/props/prop.rb', line 702 def to_desc "#{ full_name }:#{ type }" end |
#to_s ⇒ String
Returns 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”.
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 |