Class: NRSER::Props::Metadata

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

Overview

TODO:

document NRSER::Props::ClassMetadata class.

Constant Summary collapse

VARIABLE_NAME =

Constants

:@__NRSER_metadata__

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Log::Mixin

included, #logger, #logger=

Constructor Details

#initialize(klass) ⇒ Metadata

Instantiate a new ‘NRSER::Props::ClassMetadata`.



68
69
70
71
72
73
# File 'lib/nrser/props/metadata.rb', line 68

def initialize klass
  @klass = klass
  @props = {}
  @invariants = Set.new
  @storage = nil
end

Class Method Details

.has_metadata?(klass) ⇒ Boolean

Class Methods

Returns:

  • (Boolean)


50
51
52
# File 'lib/nrser/props/metadata.rb', line 50

def self.has_metadata? klass
  klass.instance_variable_defined? VARIABLE_NAME
end

.metadata_for(klass) ⇒ Object



55
56
57
# File 'lib/nrser/props/metadata.rb', line 55

def self. klass
  klass.instance_variable_get VARIABLE_NAME
end

Instance Method Details

#default_storageObject



350
351
352
353
354
355
356
# File 'lib/nrser/props/metadata.rb', line 350

def default_storage
  if superclass_has_metadata?
    .storage
  else
    @storage = NRSER::Props::Storage::InstanceVariable.new
  end
end

#each_primary_prop_value_from(values, &block) ⇒ Object

Check primary prop values and fill in defaults, yielding ‘(Prop, VALUE)` to the `&block`.

Used when initializing instances.

Parameters:

  • values (#each_pair | #each_index)

    Collection of prop values iterable by key/value pairs or by indexed entries.

  • block (Proc<(NRSER::Props::Prop, VALUE)>)

    Block that will receive primary prop and value pairs.

Raises:

  • (TypeError)

    If a value is does not satisfy it’s Prop#type.

  • (ArgumentError)

    If ‘values` doesn’t respond to ‘#each_pair` or `#each_index`.

  • (NameError)

    If a value is not provided for a primary prop and a default can not be created.

  • (TSort::Cyclic)

    If any of the primary prop’s Prop#deps for dependency cycles.



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/nrser/props/metadata.rb', line 215

def each_primary_prop_value_from values, &block
  primary_props = props only_primary: true
  
  props_by_names_and_aliases = {}
  primary_props.each_value do |prop|
    [prop.name, *prop.aliases].each do |sym|
      if props_by_names_and_aliases.key? sym
        other_prop = props_by_names_and_aliases[sym]
        
        prop_sym_is = sym == prop.name ? 'name' : 'alias'
        other_prop_sym_is = sym == other_prop.name ? 'name' : 'alias'
        
        raise NRSER::ConflictError.new \
          "Prop",  prop.to_desc, prop_sym_is, sym.inspect,
          "conflicts with", other_prop.to_desc, "of", other_prop_sym_is
      end
      
      props_by_names_and_aliases[sym] = prop
    end
  end
  
  # Normalize values to a `Hash<Symbol, VALUE>` so everything can deal with
  # one form. Default values will be set here as they're resolved and made
  # available to subsequent {Prop#default} calls.
  values_by_name = {}
  
  if values.respond_to? :each_pair
    values.each_pair { |key, value|
      # Figure out the prop name {Symbol}
      name = case key
      when Symbol
        key
      when String
        key.to_sym
      else
        key.to_s.to_sym
      end
      
      # If the `name` corresponds to a primary prop set it in the values by
      # name
      # 
      if props_by_names_and_aliases.key? name
        values_by_name[ props_by_names_and_aliases[name].name ] = value
      end
    }
  elsif values.respond_to? :each_index
    indexed = []
    
    primary_props.each_value do |prop|
      indexed[prop.index] = prop unless prop.index.nil?
    end
    
    values.each_index { |index|
      prop = indexed[index]
      values_by_name[prop.name] = values[index] if prop
    }
  else
    raise ArgumentError.new binding.erb <<~END
      `source` argument must respond to `#each_pair` or `#each_index`
      
      Found:
      
          <%= source.pretty_inspect %>
      
    END
  end
  
  # Way to noisy, even for trace
  # logger.trace "Ready to start loading values",
  #   values: values,
  #   values_by_name: values_by_name,
  #   props_by_names_and_aliases: props_by_names_and_aliases.map { |k, v|
  #     [ k, v.to_desc ]
  #   }.to_h
  
  # Topological sort the primary props by their default dependencies.
  # 
  NRSER::Graph::TSorter.new(
    primary_props.each_value
  ) { |prop, &on_dep_prop|
    # 
    # This block is responsible for receiving a {Prop} and a callback
    # block and invoking that callback block on each of the prop's
    # dependencies (if any).
    # 
    prop.deps.each { |name|
      if primary_props.key? name
        on_dep_prop.call primary_props[name]
      else
        raise RuntimeError.new binding.erb <<~END
          Property <%= prop.full_name %> depends on prop `<%= name %>`,
          but no primary prop with that name could be found!
        END
      end
    }
  }.tsort_each do |prop|
    # {Prop} instances will now be yielded in an order that allows any
    # inter-dependencies to be resolved (as long as there weren't dependency
    # cycles, which {NRSER::Graph::TSorter} will raise if it finds)
    
    # If we have a value for the prop, just check that
    if values_by_name.key? prop.name
      prop.check! values_by_name[prop.name]
    else
      # Otherwise, get the default value, providing the values we already
      # know in case the default is a {Proc} that needs some of them.
      # 
      # We set that value in `values_by_name` so that subsequent
      # {Prop#default} calls can use it.
      # 
      values_by_name[prop.name] = prop.default **values_by_name
    end
    
    # Yield the {Prop} and it's value back to the `&block`
    block.call prop, values_by_name[prop.name]
  end # .tsort_each
end

#freezeObject



372
373
374
375
376
377
378
379
380
# File 'lib/nrser/props/metadata.rb', line 372

def freeze
  super()

  if  superclass_has_metadata? &&
      !.frozen?
    .freeze
  end
  
end

#get_prop(name) ⇒ Object Also known as: []



89
90
91
92
93
94
95
96
97
98
99
# File 'lib/nrser/props/metadata.rb', line 89

def get_prop name
  name = name.to_s.to_sym unless name.is_a?( Symbol )
  
  return @props[name] if @props.key?( name )
  
  if superclass_has_metadata?
    .get_prop name
  else
    nil
  end
end

#invariant(type) ⇒ Object



345
346
347
# File 'lib/nrser/props/metadata.rb', line 345

def invariant type
  @invariants.add type
end

#invariants(only_own: false) ⇒ Object



334
335
336
337
338
339
340
341
342
# File 'lib/nrser/props/metadata.rb', line 334

def invariants only_own: false
  result = if !only_own && superclass_has_metadata?
    .invariants only_own: false
  else
    Set.new
  end
  
  result + @invariants
end

#prop(name, **opts) ⇒ NRSER::Props::Prop

Define a property.

Parameters:

Returns:

  • (NRSER::Props::Prop)

    The newly created prop, thought you probably don’t need it (it’s already all bound up on the class at this point), but why not?



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
# File 'lib/nrser/props/metadata.rb', line 153

def prop name, **opts
  t.non_empty_sym.check! name
  
  if @props.key? name
    raise ArgumentError.new binding.erb <<~END
      Prop <%= name.inspect %> already set for <%= @klass %>:
      
          <%= @props[name].inspect %>
    END
  end
  
  prop = NRSER::Props::Prop.new @klass, name, **opts
  @props[name] = prop
  
  prop.names.each do |name|
    if prop.create_reader? name
      @klass.class_eval do
        define_method name do
          prop.get self
        end
      end
    end
    
    if prop.create_writer? name
      @klass.class_eval do
        define_method "#{ name }=" do |value|
          prop.set self, value
        end
      end
    end
  end
  
  prop
end

#prop_names(only_own: false, only_primary: false) ⇒ Object

TODO:

Cache / optimize



107
108
109
# File 'lib/nrser/props/metadata.rb', line 107

def prop_names only_own: false, only_primary: false
  Set.new props( only_own: only_own, only_primary: only_primary ).keys
end

#props(only_own: false, only_primary: false) ⇒ Hash{ Symbol => NRSER::Props::Prop }

Get a map of property names to property instances.

Parameters:

  • only_own (Boolean) (defaults to: false)

    Don’t include super-class properties.

  • only_primary (Boolean) (defaults to: false)

    Don’t include properties that have a Prop#source.

Returns:



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/nrser/props/metadata.rb', line 123

def props only_own: false, only_primary: false
  result = if !only_own && superclass_has_metadata?
    .props only_own: only_own,
                              only_primary: only_primary
  else
    {}
  end
  
  if only_primary
    @props.each {|name, prop| result[name] = prop if prop.primary? }
  else
    result.merge! @props
  end
  
  result
end

#storage(value = nil) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
# File 'lib/nrser/props/metadata.rb', line 359

def storage value = nil
  if value.nil?
    if @storage.nil?
      default_storage
    else
      @storage
    end
  else
    @storage = value
  end
end

#superclass_has_metadata?Boolean

Instance Methods

Returns:

  • (Boolean)


79
80
81
# File 'lib/nrser/props/metadata.rb', line 79

def superclass_has_metadata?
  @klass.superclass.respond_to? :metadata
end

#superclass_metadataObject



84
85
86
# File 'lib/nrser/props/metadata.rb', line 84

def 
   @klass.superclass.
end