Module: Plumb::Attributes::ClassMethods

Defined in:
lib/plumb/attributes.rb

Constant Summary collapse

MUST_BE_HASH =
['Must be a Hash of attributes'].freeze

Instance Method Summary collapse

Instance Method Details

#[](type_specs) ⇒ Object

Person = Data[:name => String, :age => Integer, title?: String]



271
272
273
274
275
276
277
278
# File 'lib/plumb/attributes.rb', line 271

def [](type_specs)
  type_specs = type_specs._schema if type_specs.is_a?(Plumb::HashClass)
  klass = Class.new(self)
  type_specs.each do |key, type|
    klass.attribute(key, type)
  end
  klass
end

#__plumb_define_attribute_reader_method__(name) ⇒ Object



342
343
344
# File 'lib/plumb/attributes.rb', line 342

def __plumb_define_attribute_reader_method__(name)
  define_method(name) { @attributes[name] }
end

#__plumb_define_attribute_writer_method__(name) ⇒ Object



346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/plumb/attributes.rb', line 346

def __plumb_define_attribute_writer_method__(name)
  define_method("#{name}=") do |value|
    type = self.class._schema.at_key(name)
    result = type.resolve(value)
    @attributes[name] = result.value
    if result.valid?
      @errors.delete(name)
    else
      @errors.merge!(name => result.errors)
    end
    result.value
  end
end

#__set_nested_class__(name, klass) ⇒ Object



378
379
380
381
# File 'lib/plumb/attributes.rb', line 378

def __set_nested_class__(name, klass)
  name = name.to_s.split('_').map(&:capitalize).join.sub(/s$/, '')
  const_set(name, klass) unless const_defined?(name)
end

#_pipelineObject



201
202
203
# File 'lib/plumb/attributes.rb', line 201

def _pipeline
  @_pipeline || Plumb::Types::Any
end

#_schemaObject



238
239
240
# File 'lib/plumb/attributes.rb', line 238

def _schema
  @_schema ||= HashClass.new
end

#_set_pipeline(pl) ⇒ Object



197
198
199
# File 'lib/plumb/attributes.rb', line 197

def _set_pipeline(pl)
  @_pipeline = pl
end

#attribute(name, type = Types::Any, writer: false, &block) ⇒ Object

attribute(:friend) { attribute(:name, String) } attribute(:friend, MyStruct) { attribute(:name, String) } attribute(:name, String) attribute(:friends, Types::Array) { attribute(:name, String) } attribute(:friends, Types::Array) # same as Types::Array attribute(:friends, []) # same as Types::Array attribute(:friends, Types::Array) attribute(:friends, [Person])



308
309
310
311
312
313
314
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
# File 'lib/plumb/attributes.rb', line 308

def attribute(name, type = Types::Any, writer: false, &block)
  # Key accepts String or Symbol, with optional '?' suffix for optional keys
  # for Data structs, we always convert to Symbol keys
  key = Key.wrap(name, symbolize: true)
  name = key.to_sym
  type = Composable.wrap(type)

  if block_given? # :foo, Array[Data] or :foo, Struct
    type = __plumb_struct_class__ if type == Types::Any
    type = Plumb.decorate(type) do |node|
      if node.is_a?(Plumb::ArrayClass)
        child = node.children.first
        child = __plumb_struct_class__ if child == Types::Any
        Types::Array[build_nested(name, child, &block)]
      # A wrapper function holds a caller-supplied callable — possibly a struct
      # class, possibly not; #build_nested raises if it isn't. Deliberately NOT
      # every Function: a #transform / #build / coercion wraps a lambda Plumb
      # built, which a nested-attributes block has no business rewriting.
      elsif (node.is_a?(Plumb::Function) && node.wraps_callable?) ||
            (node.is_a?(Class) && node <= Plumb::Attributes)
        build_nested(name, node, &block)
      else
        node
      end
    end
  end

  @_schema = _schema + { key => type }
  __plumb_define_attribute_reader_method__(name)
  return name unless writer

  __plumb_define_attribute_writer_method__(name)
end

#attribute?(name, *args, &block) ⇒ Boolean

Returns:

  • (Boolean)


360
361
362
# File 'lib/plumb/attributes.rb', line 360

def attribute?(name, *args, &block)
  attribute(Key.new(name, optional: true), *args, &block)
end

#build_nested(name, node, &block) ⇒ Object



364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/plumb/attributes.rb', line 364

def build_nested(name, node, &block)
  klass = Plumb::Attributes.struct_class(node)
  unless klass
    raise ArgumentError,
          "attribute #{name.inspect} was given a nested-attributes block, " \
          "but its type #{node.inspect} is not a struct class"
  end

  sub = Class.new(klass)
  sub.instance_exec(&block)
  __set_nested_class__(name, sub)
  Composable.wrap(sub)
end

#call(result) ⇒ Plumb::Result

The Plumb::Callable interface

Parameters:

Returns:



255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/plumb/attributes.rb', line 255

def call(result)
  return result if result.value.is_a?(self)
  return result.invalid(errors: MUST_BE_HASH) unless result.value.respond_to?(:to_h)

  # Some #to_h implementations reject malformed contents; treat that as invalid input.
  begin
    attributes = result.value.to_h
  rescue ::TypeError, ::ArgumentError
    return result.invalid(errors: MUST_BE_HASH)
  end

  instance = new(attributes)
  instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors.to_h)
end

#inherited(subclass) ⇒ Object



242
243
244
245
246
247
248
# File 'lib/plumb/attributes.rb', line 242

def inherited(subclass)
  subclass._set_pipeline _pipeline
  _schema._schema.each do |key, type|
    subclass.attribute(key, type)
  end
  super
end

#node_nameObject

node name for visitors



281
# File 'lib/plumb/attributes.rb', line 281

def node_name = :data

#step(st = nil, &block) ⇒ Class

Add a step to the processing pipeline that runs before attribute validation. This allows you to transform or validate the input data before it's assigned to attributes.

Examples:

Transform input before validation

class Person
  include Plumb::Attributes

  step { |result| result.valid(result.value.transform_keys(&:to_sym)) }
  attribute :name, Types::String
end

Add custom validation

class Person
  include Plumb::Attributes

  step do |result|
    if result.value[:name].nil?
      result.invalid(errors: 'Name is required')
    else
      result
    end
  end
  attribute :name, Types::String
end

Parameters:

  • st (Plumb::Composable, #call, nil) (defaults to: nil)

    A step object to add to the pipeline

  • block (Proc, nil)

    A block to use as a step (if st is nil)

Returns:

  • (Class)

    Returns self for method chaining



233
234
235
236
# File 'lib/plumb/attributes.rb', line 233

def step(st = nil, &block)
  @_pipeline = _pipeline >> (st || block)
  self
end

#subtype_of?(other) ⇒ Boolean

A Data type joins Composable via extend, so it doesn't pick up the Equality hooks the subtype engine calls. Structural subtyping delegates to the underlying schema (a HashClass): a Data type is a subtype of other exactly when its schema is. other may be another Data type (compared schema-to-schema), a HashClass, or any Plumb type. Recurse via Plumb::Subtyping.subtype?, never #<= (which on a Class is Ruby's own class-hierarchy operator).

Returns:

  • (Boolean)


290
291
292
293
# File 'lib/plumb/attributes.rb', line 290

def subtype_of?(other)
  other = other._schema if other.respond_to?(:node_name) && other.node_name == :data
  Plumb::Subtyping.subtype?(_schema, other)
end

#supertype_of?(other) ⇒ Boolean

Mirror hook (see Composable#supertype_of?). A Data type claims a subtype only where its schema does — which by default is nothing.

Returns:

  • (Boolean)


297
# File 'lib/plumb/attributes.rb', line 297

def supertype_of?(other) = _schema.supertype_of?(other)