class Praxis::Blueprint

Attributes

attribute[R]
options[R]
reference[RW]
views[R]
decorators[RW]
object[RW]
validating[R]

Public Class Methods

_finalize!() click to toggle source

Internal finalize! logic

Calls superclass method
# File lib/praxis-blueprints/blueprint.rb, line 216
def self._finalize!
  if @block
    self.define_attribute!
    self.define_readers!
    # Don't blindly override a master view if the MediaType wants to define it on its own
    self.generate_master_view! unless self.view(:master)
    self.resolve_domain_model!
  end
  super
end
as_json_schema(**args) click to toggle source

Delegates the json-schema methods to the underlying attribute/member_type

# File lib/praxis-blueprints/blueprint.rb, line 367
def self.as_json_schema(**args)
  @attribute.type.as_json_schema(args)
end
attributes(opts = {}, &block) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 88
def self.attributes(opts = {}, &block)
  if block_given?
    raise 'Redefining Blueprint attributes is not currently supported' if self.const_defined?(:Struct, false)

    if opts.key?(:reference) && opts[:reference] != self.reference
      raise "Reference mismatch in #{self.inspect}. Given :reference option #{opts[:reference].inspect}, while using #{self.reference.inspect}"
    elsif self.reference
      opts[:reference] = self.reference # pass the reference Class down
    else
      opts[:reference] = self
    end

    @options.merge!(opts)
    @block = block

    return @attribute
  end

  raise "@attribute not defined yet for #{self.name}" unless @attribute

  @attribute.attributes
end
cache() click to toggle source

Fetch current blueprint cache, scoped by this class

# File lib/praxis-blueprints/blueprint.rb, line 160
def self.cache
  Thread.current[:praxis_blueprints_cache][self]
end
cache=(cache) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 164
def self.cache=(cache)
  Thread.current[:praxis_blueprints_cache] = cache
end
caching_enabled=(caching_enabled) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 155
def self.caching_enabled=(caching_enabled)
  @@caching_enabled = caching_enabled
end
caching_enabled?() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 151
def self.caching_enabled?
  @@caching_enabled
end
check_option!(name, value) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 116
def self.check_option!(name, value)
  case name
  when :identity
    raise Attributor::AttributorException, "Invalid identity type #{value.inspect}" unless value.is_a?(::Symbol)
    return :ok
  else
    return Attributor::Struct.check_option!(name, value)
  end
end
define_attribute!() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 233
def self.define_attribute!
  @attribute = Attributor::Attribute.new(Attributor::Struct, @options, &@block)
  @block = nil
  @attribute.type.anonymous_type true
  self.const_set(:Struct, @attribute.type)
end
define_reader!(name) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 251
def self.define_reader!(name)
  attribute = self.attributes[name]
  # TODO: profile and optimize
  # because we use the attribute in the reader,
  # it's likely faster to use define_method here
  # than module_eval, but we should make sure.
  define_method(name) do
    if @decorators && @decorators.respond_to?(name)
      @decorators.send(name)
    else
      value = @object.__send__(name)
      return value if value.nil? || value.is_a?(attribute.type)
      attribute.load(value)
    end
  end
end
define_readers!() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 240
def self.define_readers!
  self.attributes.each do |name, _attribute|
    name = name.to_sym

    # Don't redefine existing methods
    next if self.instance_methods.include? name

    define_reader! name
  end
end
describe(shallow = false, example: nil, **opts) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 70
def self.describe(shallow = false, example: nil, **opts)
  type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name

  example = example.object if example

  description = self.attribute.type.describe(shallow, example: example, **opts).merge!(id: self.id, name: type_name)
  description.delete :anonymous # discard the Struct's view of anonymity, and use the Blueprint's one
  description[:anonymous] = @_anonymous unless @_anonymous.nil?

  unless shallow
    description[:views] = self.views.each_with_object({}) do |(view_name, view), hash|
      hash[view_name] = view.describe
    end
  end

  description
end
domain_model(klass = nil) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 111
def self.domain_model(klass = nil)
  return @domain_model if klass.nil?
  @domain_model = klass
end
dump(object, view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 205
def self.dump(object, view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
  object = self.load(object, context, **opts)
  return nil if object.nil?

  object.render(view: view, context: context, **opts)
end
Also aliased as: render
example(context = nil, **values) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 173
def self.example(context = nil, **values)
  context = case context
            when nil
              ["#{self.name}-#{values.object_id}"]
            when ::String
              [context]
            else
              context
            end

  self.new(self.attribute.example(context, values: values))
end
family() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 66
def self.family
  'hash'
end
from(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
Alias for: load
generate_master_view!() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 268
def self.generate_master_view!
  attributes = self.attributes
  view :master do
    attributes.each do |name, _attr|
      # Note: we can freely pass master view for attributes that aren't blueprint/containers because
      # their dump methods will ignore it (they always dump everything regardless)
      attribute name, view: :default
    end
  end
end
inherited(klass) click to toggle source
Calls superclass method
# File lib/praxis-blueprints/blueprint.rb, line 35
def self.inherited(klass)
  super

  klass.instance_eval do
    @views = {}
    @options = {}
    @domain_model = Object
  end
end
json_schema_type() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 371
def self.json_schema_type
  @attribute.type.json_schema_type
end
load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 126
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
  case value
  when self
    value
  when nil, Hash, String
    # Need to parse/deserialize first
    # or apply default/recursive loading options if necessary
    if (value = self.attribute.load(value, context, **options))
      self.new(value)
    end
  else
    if value.is_a?(self.domain_model) || value.is_a?(self::Struct)
      # Wrap the value directly
      self.new(value)
    else
      # Wrap the object inside the domain_model
      self.new(domain_model.new(value))
    end
  end
end
Also aliased as: from
new(object, decorators = nil) click to toggle source

Override default new behavior to support memoized creation through an IdentityMap

# File lib/praxis-blueprints/blueprint.rb, line 46
def self.new(object, decorators = nil)
  if @@caching_enabled && decorators.nil?
    cache = if object.respond_to?(:identity_map) && object.identity_map
              object.identity_map.blueprint_cache[self]
            else
              self.cache
            end

    return cache[object] ||= begin
      blueprint = self.allocate
      blueprint.send(:initialize, object, decorators)
      blueprint
    end
  end

  blueprint = self.allocate
  blueprint.send(:initialize, object, decorators)
  blueprint
end
new(object, decorators = nil) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 279
def initialize(object, decorators = nil)
  # TODO: decide what sort of type checking (if any) we want to perform here.
  @object = object

  @decorators = if decorators.is_a?(Hash) && decorators.any?
                  OpenStruct.new(decorators)
                else
                  decorators
                end

  @validating = false
end
render(object, view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
Alias for: dump
resolve_domain_model!() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 227
def self.resolve_domain_model!
  return unless self.domain_model.is_a?(String)

  @domain_model = self.domain_model.constantize
end
valid_type?(value) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 168
def self.valid_type?(value)
  # FIXME: this should be more... ducklike
  value.is_a?(self) || value.is_a?(self.attribute.type)
end
validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 186
def self.validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
  raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context.nil?
  context = [context] if context.is_a? ::String

  unless value.is_a?(self)
    raise ArgumentError, "Error validating #{Attributor.humanize_context(context)} as #{self.name} for an object of type #{value.class.name}."
  end

  value.validate(context)
end
view(name, **options, &block) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 197
def self.view(name, **options, &block)
  if block_given?
    return self.views[name] = View.new(name, self, **options, &block)
  end

  self.views[name]
end

Public Instance Methods

_cache_key() click to toggle source

By default we'll use the object identity, to avoid rendering the same object twice Override, if there is a better way cache things up

# File lib/praxis-blueprints/blueprint.rb, line 294
def _cache_key
  self.object
end
_get_attr(name) click to toggle source

generic semi-private getter used by Renderer

# File lib/praxis-blueprints/blueprint.rb, line 362
def _get_attr(name)
  self.send(name)
end
dump(view_name = nil, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **opts)
Alias for: render
render(view_name = nil, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **opts) click to toggle source

Render the wrapped data with the given view

# File lib/praxis-blueprints/blueprint.rb, line 299
def render(view_name = nil, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **opts)
  if !view_name.nil?
    warn 'DEPRECATED: please do not pass the view name as the first parameter in Blueprint.render, pass through the view: named param instead.'
  elsif opts.key?(:view)
    view_name = opts[:view]
  end

  fields = opts[:fields]
  view_name = :default if view_name.nil? && fields.nil?

  if view_name
    unless (view = self.class.views[view_name])
      raise "view with name '#{view_name.inspect}' is not defined in #{self.class}"
    end
    return view.render(self, context: context, renderer: renderer)
  end

  # Accept a simple array of fields, and transform it to a 1-level hash with true values
  if fields.is_a? Array
    fields = fields.each_with_object({}) { |field, hash| hash[field] = true }
  end

  # expand fields
  expanded_fields = FieldExpander.expand(self.class, fields)

  renderer.render(self, expanded_fields, context: context)
end
Also aliased as: dump
to_h() click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 329
def to_h
  Attributor.recursive_to_h(@object)
end
validate(context = Attributor::DEFAULT_ROOT_CONTEXT) click to toggle source
# File lib/praxis-blueprints/blueprint.rb, line 333
def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
  raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context.nil?
  context = [context] if context.is_a? ::String
  keys_with_values = []

  raise 'validation conflict' if @validating
  @validating = true

  errors = []
  self.class.attributes.each do |sub_attribute_name, sub_attribute|
    sub_context = self.class.generate_subcontext(context, sub_attribute_name)
    value = self.send(sub_attribute_name)
    keys_with_values << sub_attribute_name unless value.nil?

    if value.respond_to?(:validating) # really, it's a thing with sub-attributes
      next if value.validating
    end
    errors.concat(sub_attribute.validate(value, sub_context))
  end
  self.class.attribute.type.requirements.each do |req|
    validation_errors = req.validate(keys_with_values, context)
    errors.concat(validation_errors) unless validation_errors.empty?
  end
  errors
ensure
  @validating = false
end