Class: NRSER::Meta::Props::Prop

Inherits:
Object
  • Object
show all
Defined in:
lib/nrser/meta/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

Constructor Details

#initialize(defined_in, name, type: t.any, default: nil, default_from: nil, source: nil, to_data: nil, from_data: nil) ⇒ 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 ClassMethods#prop that is extended in to classes including NRSER::Meta::Props.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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
# File 'lib/nrser/meta/props/prop.rb', line 86

def initialize  defined_in,
                name,
                type: t.any,
                default: nil,
                default_from: nil,
                source: nil,
                to_data: nil,
                from_data: nil
  
  # Set these up first so {#to_s} works in case we need to raise errors.
  @defined_in = defined_in
  @name = name
  @type = t.make type
  
  @to_data = to_data
  @from_data = from_data
  
  # Source
  
  @source = source # TODO fix this: t.maybe( t.label ).check source
  
  # Detect if the source 
  if source.nil?
    @instance_variable_source = false
  else
    # TODO Check that default and default_from are `nil`, make no sense here
    
    source_str = source.to_s
    @instance_variable_source = source_str[0] == '@'
  end
  
  # Defaults
  
  # Can't provide both default and default_from
  unless default.nil? || default_from.nil?
    raise NRSER::ConflictError.new binding.erb "      Both `default:` and `default_from:` keyword args provided when \n      constructing <%= self %>. At least one must be `nil`.\n      \n      default:\n          <%= default.pretty_inspect %>\n      \n      default_from:\n          <%= default_from.pretty_inspect %>\n      \n    ERB\n  end\n  \n  if default_from.nil?\n    # We are going to use `default`\n        \n    # Validate `default` value\n    if default.nil?\n      # If it's `nil` we still want to see if it's a valid value for the type\n      # so we can report if this prop has a default value or not.\n      # \n      # However, we only need to do that if there is no `source`\n      # \n      @has_default = if source.nil?\n        @type.test default\n      else\n        # NOTE  This is up for debate... does a derived property have a \n        #       default? What does that even mean?\n        true # false ?\n      end\n      \n    else\n      # Check that the default value is valid for the type, raising TypeError\n      # if it isn't.\n      @type.check( default ) { |type:, value:|\n        binding.erb <<-ERB\n          Default value is not valid for <%= self %>:\n          \n              <%= value.pretty_inspect %>\n          \n        ERB\n      }\n      \n      # If we passed the check we know the value is valid\n      @has_default = true\n      \n      # Set the default value to `default`, freezing it since it will be \n      # set on instances without any attempt at duplication, which seems like\n      # it *might be ok* since a lot of prop'd classes are being used\n      # immutably.\n      @default_value = default.freeze\n    end\n    \n  else\n    # `default_from` is not `nil`, so we're going to use that.\n    \n    # This means we \"have\" a default since we believe we can use it to make\n    # one - the actual values will have to be validates at that point.\n    @has_default = true\n    \n    # And set it.\n    # \n    # TODO validate it's something reasonable here?\n    # \n    @default_from = default_from\n  end\n  \nend\n"

Instance Attribute Details

#defined_inClass (readonly)

The class the prop was defined in.

Returns:

  • (Class)


46
47
48
# File 'lib/nrser/meta/props/prop.rb', line 46

def defined_in
  @defined_in
end

#nameSymbol (readonly)

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

Returns:

  • (Symbol)


54
55
56
# File 'lib/nrser/meta/props/prop.rb', line 54

def name
  @name
end

#sourceSymbol | String (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:

  • (Symbol | String)


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

def source
  @source
end

#typeNRSER::Types::Type (readonly)

The type of the valid values for the property.

Returns:



61
62
63
# File 'lib/nrser/meta/props/prop.rb', line 61

def type
  @type
end

Instance Method Details

#defaultObject



220
221
222
223
224
225
226
227
228
# File 'lib/nrser/meta/props/prop.rb', line 220

def default
  if default?
    @default
  else
    raise NameError.new NRSER.squish "      Prop \#{ self } has no default value.\n    END\n  end\nend\n"

#full_nameString

Full name with class prop was defined in.

Examples:

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

Returns:

  • (String)


199
200
201
# File 'lib/nrser/meta/props/prop.rb', line 199

def full_name
  "#{ defined_in.name }##{ name }"
end

#get(instance) ⇒ return_type

TODO:

Document get method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/nrser/meta/props/prop.rb', line 278

def get instance
  if source?
    if instance_variable_source?
      instance.instance_variable_get source
    else
      case source
      when String, Symbol
        instance.send source
      when Proc
        instance.instance_exec &source
      else
        raise TypeError.squished "          Expected `Prop#source` to be a String, Symbol or Proc;\n          found \#{ source.inspect }\n        END\n      end\n    end\n  else\n    values(instance)[name]\n  end\nend\n"

#has_default?return_type Also known as: default?

Test if this prop is configured to provide default values -

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



212
213
214
# File 'lib/nrser/meta/props/prop.rb', line 212

def has_default?
  @has_default
end

#instance_variable_source?return_type

TODO:

Document instance_variable_source? method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



252
253
254
# File 'lib/nrser/meta/props/prop.rb', line 252

def instance_variable_source?
  @instance_variable_source
end

#primary?return_type

TODO:

Document primary? method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



265
266
267
# File 'lib/nrser/meta/props/prop.rb', line 265

def primary?
  !source?
end

#set(instance, value) ⇒ return_type

TODO:

Document set method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/nrser/meta/props/prop.rb', line 309

def set instance, value
  type.check( value ) do
    binding.erb "      Value of type <%= value.class.name %> for prop <%= self.full_name %> \n      failed type check.\n      \n      Must satisfy type:\n      \n          <%= type %>\n      \n      Given value:\n      \n          <%= value.pretty_inspect %>\n      \n    END\n  end\n  \n  values(instance)[name] = value\nend\n"

#set_from_values_hash(instance, **values) ⇒ return_type

TODO:

Document set_from_hash method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/nrser/meta/props/prop.rb', line 338

def set_from_values_hash instance, **values
  if values.key? name
    set instance, values[name]
  else
    if default?
      # set instance, if !default.nil? && default.respond_to?( :dup )
      #   default.dup
      # else
      #   default
      # end
      set instance, default
    else
      raise TypeError.new binding.erb "        Prop <#= full_name %> has no default value and no value was provided \n        in values:\n        \n            <%= values.pretty_inspect %>\n        \n      ERB\n    end\n  end\nend\n"

#source?return_type

TODO:

Document source? method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



239
240
241
# File 'lib/nrser/meta/props/prop.rb', line 239

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:

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.



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/nrser/meta/props/prop.rb', line 406

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 "      Expected `@to_data` to be Symbol, String or Proc;\n      found \#{ @to_data.inspect }\n    END\n  end\nend\n"

#to_sString

Returns a short string describing the instance.

Returns:

  • (String)

    a short string describing the instance.



492
493
494
# File 'lib/nrser/meta/props/prop.rb', line 492

def to_s
  "#<#{ self.class.name } #{ full_name }:#{ type }>"
end

#value_from_data(data) ⇒ return_type

TODO:

Document value_from_data method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/nrser/meta/props/prop.rb', line 439

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 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 @to_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 "      Expected `@from_data` to be Symbol, String or Proc;\n      found <%= @from_data.class %>.\n      \n      Acceptable types:\n      \n      -   Symbol or String\n          -   Name of class method on the class this property is defined in\n              (<%= @defined_in %>) to call with data to convert it to a\n              property value.\n              \n      -   Proc\n          -   Procedure to call with data to convert it to a property value.\n      \n      Found `@from_data`:\n      \n          <%= @from_data.pretty_inspect %>\n      \n      (type <%= @from_data.class %>)\n      \n    ERB\n  end\n    \nend\n"