Class: Params::Registry::Template

Inherits:
Object
  • Object
show all
Defined in:
lib/params/registry/template.rb

Overview

This class manages an individual parameter template. It encapsulates all the information and operations needed to validate and coerce individual parameter values, as well as for serializing them back into a string, and for doing so with bit-for-bit consistency.

A parameter template can have a human-readable Symbol as a slug, which is distinct from its canonical identifier (id), which can be any object, although that must be unique for the entire registry (while a slug only needs to be unique within its enclosing group). It can also have any number of aliases.

A template can manage a simple type like a string or number, or a composite type like an array, tuple (implemented as a fixed-length array), set, or range containing appropriate simple types. The current (provisional) way to specify the types for the template are the type and composite initialization parameters, where if the latter is present, the former will be treated as its member type.

If certain forays into open-source diplomacy go well, these can be consolidated into a single type declaration.

A parameter may depend on (depends) or conflict (conflicts) with other parameters, or even consume (consumes) them as input. The cardinality of a parameter is controlled by min and max, which default to zero and unbounded, respectively. To require a parameter, set min to an integer greater than zero, and to enforce a single scalar value, set max to 1. (Setting min greater than max will raise an error.) To control whether a value of nil or the empty string is dropped, kept (as the empty string) or kept as nil, set the empty parameter.

When max is greater than 1, the template automatically coerces any simple value into an array containing that value. (And when max is equal to 1, an array will be replaced with a single value.) Passing an array into #process with fewer than min values (or a single value when min is greater than 1) will produce an error. Whether the first N values (up to max) or the last N values are taken from the input, is controlled by the shift parameter.

Composite values begin life as arrays of simple values. During processing, the individual values are coerced from what are assumed to be strings, and then the arrays themselves are coerced into the composite types. Serialization is the same thing in reverse, using a function passed into unwind (which otherwise defaults to to_a) to turn the composite type back into an array, before the individual values being turned into strings by way of the value passed into format, which can either be a standard format string or a Proc. The unwind function is also expected to sort the array. There is also a reverse flag for when it makes sense to

The transformation process, from array of strings to composite object and back again, has a few more points of intervention. There is an optional preproc function, which is run when the Instance is processed, after the individual values are coerced and before the composite coercion is applied, and a contextualize function, which is run after unwind but before format. Both of these functions make it possible to use information from the parameter's dependencies to manipulate its values based on its context within a live Instance.

Certain composite types, such as sets and ranges, have a coherent concept of a universe, which is implemented here as a function that generates a compatible object. This is useful for when the serialized representation of a parameter can be large. For instance, if a set's universe has 100 elements and we want to represent the subset with all the elements except for element 42, rather than serializing a 99-element query string, we complement the set and make a note to that effect (to be picked up by the Instance serialization process and put in its complement parameter). The function passed into complement will be run as an instance method, which has access to universe. Other remarks about these functions:

  • The preproc and contextualize functions are expected to take the form -> value, hash { expr } and must return an array. The hash here is expected to contain at least the subset of parameters marked as dependencies (as is done in Instance), keyed by slug or, failing that, id.
  • The unwind and complement functions both take the composite value as an argument (-> value { expr }). unwind is expected to return an array of elements, and complement should return a composite object of the same type.
  • The universe function takes no arguments and is expected to return a composite object of the same type.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(registry, id, slug: nil, type: Types::NormalizedString, composite: nil, format: nil, aliases: nil, depends: nil, conflicts: nil, consumes: nil, preproc: nil, min: 0, max: nil, shift: false, empty: false, default: nil, universe: nil, complement: nil, unwind: nil, contextualize: nil, reverse: false) ⇒ Template

Initialize the template object.

Parameters:

  • registry (Params::Registry)

    A backreference to the parameter registry

  • id (Object)

    The canonical, unique identifier for the parameter

  • slug (#to_sym) (defaults to: nil)

    A "friendly" symbol that will end up in the serialization

  • aliases (Array<#to_sym>) (defaults to: nil)

    Alternative nicknames for the parameter

  • type (Dry::Types::Type, Symbol, Proc) (defaults to: Types::NormalizedString)

    An "atomic" type for single values

  • composite (Dry::Types::Type, Symbol, Proc) (defaults to: nil)

    A composite type into which multiple values are loaded

  • format (String, Proc, nil) (defaults to: nil)

    A format string or function

  • depends (Array) (defaults to: nil)

    Parameters that this one depends on

  • conflicts (Array) (defaults to: nil)

    Parameters that conflict with this one

  • consumes (Array) (defaults to: nil)

    Parameters that can be given in lieu of this one, that will be composed into this one. Parameters this one consumes implies depends and conflicts.

  • preproc (Proc, nil) (defaults to: nil)

    A preprocessing function that is fed parameters from consumes and depends to generate this parameter

  • min (Integer, nil) (defaults to: 0)

    Minimum cardinality

  • max (Integer, nil) (defaults to: nil)

    Maximum cardinality

  • shift (false, true) (defaults to: false)

    When given more than max values, do we take the ones we want from the back or from the front

  • empty (false, true, nil) (defaults to: false)

    whether to treat an empty value as nil, the empty string, or discard it

  • default (Object, nil) (defaults to: nil)

    A default value

  • universe (Proc) (defaults to: nil)

    For Set or Range composite types and derivatives, a function that returns the universal set or range

  • complement (Proc) (defaults to: nil)

    For Set or Range composite types, a function that will return the complement of the set or range

  • unwind (Proc) (defaults to: nil)

    A function that takes a composite type and turns it into an Array of atomic values

  • contextualize (Proc) (defaults to: nil)

    A function that takes an unwound composite value and modifies it based on the other parameters it depends on

  • reverse (false, true) (defaults to: false)

    For Range composite types, a flag that indicates whether the values should be interpreted and/or serialized in reverse order. Also governs the serialization of Set composites.

Raises:

  • (ArgumentError)


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/params/registry/template.rb', line 155

def initialize registry, id, slug: nil, type: Types::NormalizedString,
    composite: nil, format: nil, aliases: nil, depends: nil, conflicts: nil,
    consumes: nil, preproc: nil, min: 0, max: nil, shift: false,
    empty: false, default: nil, universe: nil, complement: nil,
    unwind: nil, contextualize: nil, reverse: false

  @registry  = Types::Registry[registry]
  @id        = Types::NonNil[id]
  @slug      = Types::Symbol[slug] if slug
  @type      = Types[type]
  @composite = Types[composite] if composite
  @format    = (Types::Proc | Types::String)[format] if format
  @aliases   = Types::Array[aliases]
  @depends   = Types::Array[depends]
  @conflicts = Types::Array[conflicts]
  @consumes  = Types::Array[consumes]
  @preproc   = Types::Proc[preproc] if preproc
  @min       = Types::NonNegativeInteger[min || 0]
  @max       = Types::PositiveInteger.optional[max]
  @shift     = Types::Bool[shift]
  @empty     = Types::Bool[empty]
  @default   = Types::Nominal::Any[default]
  @unifunc   = Types::Proc[universe]      if universe
  @comfunc   = Types::Proc[complement]    if complement
  @unwfunc   = Types::Proc[unwind]        if unwind
  @confunc   = Types::Proc[contextualize] if contextualize
  @reverse   = Types::Bool[reverse]

  raise ArgumentError, "min (#{@min}) cannot be greater than max (#{@max})" if
    @min and @max and @min > @max

  # post-initialization hook
  post_init
end

Instance Attribute Details

#aliasesArray<Symbol> (readonly)

Returns any aliases for this parameter.

Returns:

  • (Array<Symbol>)

    any aliases for this parameter.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#blank?Boolean (readonly)

Returns true if the template has no configuration data to speak of.

Returns:

  • (Boolean)


367
368
369
370
371
372
373
374
375
# File 'lib/params/registry/template.rb', line 367

def blank?
  # XXX PHEWWW
  @slug.nil? && @type == Types::NormalizedString && @composite.nil? &&
    @format.nil? && @aliases.empty? && @depends.empty? &&
    @conflicts.empty? && @consumes.empty? && @preproc.nil? &&
    @min == 0 && @max.nil? && !@shift && !@empty && @default.nil? &&
    @unifunc.nil? && @comfunc.nil? && @unwfunc.nil? && @confunc.nil? &&
    !@reverse
end

#complement?Boolean (readonly)

Whether this (composite) parameter can be complemented or inverted.

Returns:

  • (Boolean)


355
# File 'lib/params/registry/template.rb', line 355

def complement? ; !!@comfunc; end

#compositeDry::Types::Type? (readonly)

Returns the type for composite values.

Returns:

  • (Dry::Types::Type, nil)

    the type for composite values.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#composite?Boolean (readonly)

Whether this parameter is composite.

Returns:

  • (Boolean)


290
# File 'lib/params/registry/template.rb', line 290

def composite? ; !!@composite; end

#conflictsArray (readonly)

Any parameters this one conflicts with.

Returns:

  • (Array)

Raises:



274
275
276
277
278
279
280
281
282
283
# File 'lib/params/registry/template.rb', line 274

def conflicts
  out = (@conflicts | (@preproc ? @consumes : [])).map do |t|
    registry.templates.canonical t
  end

  raise E::Internal, "Malformed conflict declaration on #{id}" if
    out.any?(&:nil?)

  out
end

#consumesArray (readonly)

Any parameters this one consumes (implies depends + conflicts).

Returns:

  • (Array)

Raises:



311
312
313
314
315
316
317
318
# File 'lib/params/registry/template.rb', line 311

def consumes
  out = @consumes.map { |t| registry.templates.canonical t }

  raise E::Internal,
    "Malformed consumes declaration on #{id}" if out.any?(&:nil?)

  out
end

#contextualize? Whether there is a contextualizing(Whetherthereisacontextualizing) ⇒ Boolean (readonly)

function present in the unprocessing stack.

Returns:

  • (Boolean)


362
# File 'lib/params/registry/template.rb', line 362

def contextualize? ; !!@confunc; end

#defaultObject (readonly)

Returns the value of attribute default.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#default?Boolean (readonly)

Whether this parameter has a default value.

Returns:

  • (Boolean)


297
# File 'lib/params/registry/template.rb', line 297

def default? ; !@default.nil?; end

#dependsArray (readonly)

Any parameters this one depends on.

Returns:

  • (Array)

Raises:



258
259
260
261
262
263
264
265
266
267
# File 'lib/params/registry/template.rb', line 258

def depends
  out = (@depends | (@preproc ? @consumes : [])).map do |t|
    registry.templates.canonical t
  end

  raise E::Internal, "Malformed dependency declaration on #{id}" if
    out.any?(&:nil?)

  out
end

#empty?Boolean (readonly)

Whether to accept empty values.

Returns:

  • (Boolean)


341
# File 'lib/params/registry/template.rb', line 341

def empty? ; !!@empty; end

#idObject (readonly)

Returns the canonical identifier for the parameter.

Returns:

  • (Object)

    the canonical identifier for the parameter.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#maxInteger? (readonly)

Returns maximum cardinality for the parameter's values.

Returns:

  • (Integer, nil)

    maximum cardinality for the parameter's values.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#minInteger (readonly)

Returns minimum cardinality for the parameter's values.

Returns:

  • (Integer)

    minimum cardinality for the parameter's values.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#preproc(myself, others) ⇒ Array (readonly)

Preprocess a parameter value against itself and/or consumed values.

Parameters:

  • myself (Array)

    raw values for the parameter itself.

  • others (Array)

    processed values for the consumed parameters.

Returns:

  • (Array)

    pseudo-raw, preprocessed values for the parameter.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#preproc?Boolean (readonly)

Whether there is a preprocessor function.

Returns:

  • (Boolean)


304
# File 'lib/params/registry/template.rb', line 304

def preproc? ; !!@preproc ; end

#registryParams::Registry (readonly)

Returns a backreference to the registry.

Returns:



221
222
223
# File 'lib/params/registry/template.rb', line 221

def registry
  @registry
end

#reverse?Boolean (readonly)

Whether to interpret composite values as reversed.

Returns:

  • (Boolean)


348
# File 'lib/params/registry/template.rb', line 348

def reverse? ; !!@reverse; end

#shift?Boolean (readonly)

Whether to shift values more than max cardinality off the front.

Returns:

  • (Boolean)


334
# File 'lib/params/registry/template.rb', line 334

def shift? ; !!@shift; end

#slugSymbol? (readonly)

Returns the primary nickname for the parameter, if different from the id.

Returns:

  • (Symbol, nil)

    the primary nickname for the parameter, if different from the id.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#typeDry::Types::Type (readonly)

Returns the type for individual parameter values.

Returns:

  • (Dry::Types::Type)

    the type for individual parameter values.



221
222
# File 'lib/params/registry/template.rb', line 221

attr_reader :registry, :id, :slug, :type, :composite, :aliases,
:preproc, :min, :max, :default

#universeObject? (readonly)

The universal composite object (e.g. set or range) from which valid values are drawn.

Returns:

  • (Object, nil)


324
325
326
327
# File 'lib/params/registry/template.rb', line 324

def universe
  refresh! if @unifunc and not @universe
  @universe
end

Instance Method Details

#complement(value, unwind: false) ⇒ Object?

Return the complement of the composite value for the parameter.

Parameters:

  • value (Object)

    the composite object to complement.

  • unwind (false, true) (defaults to: false)

    whether or not to also apply unwind

Returns:

  • (Object, nil)

    the complementary object, if a complement is defined.



422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/params/registry/template.rb', line 422

def complement value, unwind: false
  return unless @comfunc

  begin
    out = instance_exec value, &@comfunc
  rescue e
    raise E::Internal.new(
      "Complement function on #{id} failed: #{e.message}",
      context: self, value: value, original: e)
  end

  unwind ? self.unwind(out) : out
end

#contextualize?Boolean

function present in the unprocessing stack.

Returns:

  • (Boolean)


362
# File 'lib/params/registry/template.rb', line 362

def contextualize? ; !!@confunc; end

#format(scalar) ⇒ String

Format an individual atomic value.

Parameters:

  • scalar (Object)

    the scalar/atomic value.

Returns:

  • (String)

    serialized to a string.



405
406
407
408
409
410
411
412
413
# File 'lib/params/registry/template.rb', line 405

def format scalar
  return scalar.to_s unless @format

  if @format.is_a? Proc
    instance_exec scalar, &@format
  else
    (@format || '%s').to_s % scalar
  end
end

#inspectString

Return a suitable representation for debugging.

Returns:

  • (String)

    the object.



574
575
576
577
578
579
580
# File 'lib/params/registry/template.rb', line 574

def inspect
  c = self.class
  i = id.inspect
  t = '%s%s' % [type, composite ? ", #{composite}]" : '']

  "#<#{c} #{i} (#{t})>"
end

#process(value) ⇒ Object, Array

Validate a list of individual parameter values and (if one is present) construct a composite value.

Parameters:

  • value (Object)

    the values given for the parameter.

Returns:

  • (Object, Array)

    the processed value(s).

Raises:



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
487
488
489
490
491
492
493
494
495
# File 'lib/params/registry/template.rb', line 443

def process value
  # XXX what we _really_ want is `Types::Set.of` and
  # `Types::Range.of` but who the hell knows how to actually make
  # that happen, so what we're gonna do instead is test if the
  # template is composite, then test the input against the composite
  # type, then run `unwind` on it and test the individual members

  # warn [(slug || id), value].inspect

  # coerce and then unwind
  value = unwind composite[value] if composite?

  # otherwise coerce into an array
  value = [value] unless value.is_a? Array

  # copy to out
  out = []

  value.each do |v|
    # skip over nil/empty values unless we can be empty
    if v.nil? or v.to_s.empty?
      next unless empty?
      v = nil
    end

    if v
      begin
        tmp = type[v] # this either crashes or it doesn't
        v = tmp # in which case v is only assigned if successful
      rescue Dry::Types::CoercionError => e
        raise E::Syntax.new e.message, context: self, value: v, original: e
      end
    end

    out << v
  end

  # now we deal with cardinality
  raise E::Cardinality,
    "Need #{min} values and there are only #{out.length} values" if
    out.length < min

  # warn "hurr #{out.inspect}, #{max}"

  if max
    # return if it's supposed to be a scalar value
    return out.first if max == 1
    # cut the values to length from either the front or back
    out.slice!((shift? ? -max : 0), max) if out.length > max
  end

  composite ? composite[out] : out
end

#refresh!void

This method returns an undefined value.

Refreshes stateful information like the universal set, if present.



557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/params/registry/template.rb', line 557

def refresh!
  if @unifunc
    # do we want to call or do we want to instance_exec?
    univ = @unifunc.call

    univ = composite[univ] if composite?

    @universe = univ
  end

  self
end

#unprocess(value, dependencies = {}, try_complement: false) ⇒ Array<String>, ...

This method takes a value which is assumed to be valid and transforms it into an array of strings suitable for serialization.

Applies unwind to value to get an array, then format over each of the elements to get strings. If scalar is true, it will also return the flag from unwind indicating whether or not the complement parameter should be set.

This method is called by Instance#to_s and others to produce content which is amenable to serialization. As what happens there, the content of rest should be the values of the parameters specified in depends.

Parameters:

  • value (Object, Array<Object>)

    The parameter value(s).

  • dependencies (Hash) (defaults to: {})

    The rest of the parameter values.

  • try_complement (false, true) (defaults to: false)

    Whether to attempt to complement a composite vlaue and return the complement flag in addition to the unwound values.

Returns:

  • (Array<String>, Array<(Array<String>, false)>, Array<(Array<String>, true)>, nil)

    the unwound value(s), plus optionally the complement flag, or otherwise nil.



520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
# File 'lib/params/registry/template.rb', line 520

def unprocess value, dependencies = {}, try_complement: false
  # we begin assuming the value has not been complemented
  comp = false

  if composite?
    # coerce just to be sure
    value = composite[value]
    # now unwind
    value = unwind value

    if try_complement and complement?
      tmp = complement value, unwind: true
      if tmp.length < value.length
        value = tmp
        comp  = true
      end
    end
  else
    value = [value] unless value.is_a? Array
  end

  # now we contextualize
  value = contextualize value if contextualize?

  # now we maybe prune out blanks
  value.compact! unless empty?
  # any nil at this point is on purpose
  value.map! { |v| v.nil? ? '' : self.format(v) }

  # now we return the pair if we're trying to complement it
  try_complement ? [value, comp] : value
end

#unwind(value) ⇒ Array

Unwind a composite value into an array of simple values. This wraps the matching function passed into the constructor, or #to_a in lieu of one.

Parameters:

  • value (Object)

    a composite value

Returns:

  • (Array)

    the unwound composite

Raises:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/params/registry/template.rb', line 234

def unwind value
  return unless composite?

  func = @unwfunc || -> v { v.to_a }

  begin
    out = instance_exec value, &func
  rescue Exception, e
    raise E::Internal.new "Unwind on #{id} failed: #{e.message}",
      value: value, original: e
  end

  raise E::Internal,
    "Unwind on #{id} returned #{out.class}, not an Array" unless
    out.is_a? Array

  out
end