class Praxis::Blueprint

Attributes

attribute[R]
options[R]
object[RW]
validating[R]

Public Class Methods

_finalize!() click to toggle source

Internal finalize! logic

Calls superclass method
# File lib/praxis/blueprint.rb, line 227
def self._finalize!
  if @block
    define_attribute!
    define_readers!
    # Don't blindly override a the default fieldset if the MediaType wants to define it on its own
    if @block_for_default_fieldset
      parse_default_fieldset(@block_for_default_fieldset)
    else
      generate_default_fieldset!
    end
    resolve_domain_model!
  end
  # Make sure to add the given defined description to the underlying type, so it can show up in the docs, etc
  # Blueprint groups do not have a description...
  if respond_to?(:description) && description
    options[:description] = description
    @attribute.type.options[:description] = description
  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/blueprint.rb, line 382
def self.as_json_schema(**args)
  # TODO: Aren't we loosing the attribute options if we just call the type?? (e.g. description, etc)
  # Also, we might want to add a 'title' for MTs, to be the class name (without prefixing) ...
  @attribute.type.as_json_schema(args)
end
attributes(opts = {}, &block) click to toggle source
# File lib/praxis/blueprint.rb, line 98
def self.attributes(opts = {}, &block)
  if block_given?
    raise 'Redefining Blueprint attributes is not currently supported' if const_defined?(:InnerStruct, false)

    @options.merge!(opts.merge(dsl_compiler: DSLCompiler))
    @block = block

    return @attribute
  end

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

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

Fetch current blueprint cache, scoped by this class

# File lib/praxis/blueprint.rb, line 155
def self.cache
  Thread.current[:praxis_blueprints_cache][self]
end
cache=(cache) click to toggle source
# File lib/praxis/blueprint.rb, line 159
def self.cache=(cache)
  Thread.current[:praxis_blueprints_cache] = cache
end
caching_enabled=(caching_enabled) click to toggle source
# File lib/praxis/blueprint.rb, line 150
def self.caching_enabled=(caching_enabled)
  @@caching_enabled = caching_enabled # rubocop:disable Style/ClassVars
end
caching_enabled?() click to toggle source
# File lib/praxis/blueprint.rb, line 146
def self.caching_enabled?
  @@caching_enabled
end
check_option!(name, value) click to toggle source
# File lib/praxis/blueprint.rb, line 119
def self.check_option!(name, value)
  Attributor::Struct.check_option!(name, value)
end
default_fieldset(&block) click to toggle source
# File lib/praxis/blueprint.rb, line 190
def self.default_fieldset(&block)
  return @default_fieldset unless block_given?

  @block_for_default_fieldset = block
end
define_attribute!() click to toggle source
# File lib/praxis/blueprint.rb, line 254
def self.define_attribute!
  @attribute = Attributor::Attribute.new(Attributor::Struct, @options, &@block)
  @block = nil
  @attribute.type.anonymous_type true
  const_set(:InnerStruct, @attribute.type)
end
define_reader!(name) click to toggle source
# File lib/praxis/blueprint.rb, line 272
def self.define_reader!(name)
  attribute = 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
    value = @object.__send__(name)
    return value if value.nil? || value.is_a?(attribute.type)

    attribute.load(value)
  end
end
define_readers!() click to toggle source
# File lib/praxis/blueprint.rb, line 261
def self.define_readers!
  attributes.each do |name, _attribute|
    name = name.to_sym

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

    define_reader! name
  end
end
domain_model(klass = nil) click to toggle source
# File lib/praxis/blueprint.rb, line 113
def self.domain_model(klass = nil)
  return @domain_model if klass.nil?

  @domain_model = klass
end
dump(object, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts) click to toggle source

renders using the implicit default fieldset

# File lib/praxis/blueprint.rb, line 215
def self.dump(object, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
  object = self.load(object, context, **opts)
  return nil if object.nil?

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

  new(attribute.example(context, values: values))
end
family() click to toggle source
# File lib/praxis/blueprint.rb, line 94
def self.family
  'hash'
end
from(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
Alias for: load
generate_default_fieldset!() click to toggle source
# File lib/praxis/blueprint.rb, line 286
def self.generate_default_fieldset!
  attributes = self.attributes

  @default_fieldset = {}
  attributes.each do |name, attr|
    the_type = attr.type < Attributor::Collection ? attr.type.member_type : attr.type
    next if the_type < Blueprint

    # TODO: Allow groups in the default fieldset?? or perhaps better to make people explicitly define them?
    # next if (the_type < Blueprint && !(the_type < BlueprintAttributeGroup))

    # NOTE: we won't try to expand fields here, as we want to be lazy (and we're expanding)
    # every time a request comes in anyway. This could be an optimization we do at some point
    # or we can 'memoize it' to avoid trying to expand it over an over...
    @default_fieldset[name] = true
  end
end
inherited(klass) click to toggle source
Calls superclass method
# File lib/praxis/blueprint.rb, line 68
def self.inherited(klass)
  super

  klass.instance_eval do
    @options = {}
    @domain_model = Object
    @default_fieldset = {}
  end
end
json_schema_type() click to toggle source
# File lib/praxis/blueprint.rb, line 388
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/blueprint.rb, line 123
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
  case value
  when self
    value
  when nil, Hash, String
    if (value = attribute.load(value, context, **options))
      new(value)
    end
  else
    if value.is_a?(domain_model) || value.is_a?(self::InnerStruct)
      # Wrap the value directly
      new(value)
    else
      # Wrap the object inside the domain_model
      new(domain_model.new(value))
    end
  end
end
Also aliased as: from
new(object) click to toggle source

Override default new behavior to support memoized creation through an IdentityMap

# File lib/praxis/blueprint.rb, line 79
def self.new(object)
  # TODO: do we want to allow the identity map thing in the object?...maybe not.
  if @@caching_enabled
    return cache[object] ||= begin
      blueprint = allocate
      blueprint.send(:initialize, object)
      blueprint
    end
  end

  blueprint = allocate
  blueprint.send(:initialize, object)
  blueprint
end
new(object) click to toggle source
# File lib/praxis/blueprint.rb, line 304
def initialize(object)
  @object = object
  @validating = false
end
parse_default_fieldset(block) click to toggle source
# File lib/praxis/blueprint.rb, line 209
def self.parse_default_fieldset(block)
  @default_fieldset = FieldsetParser.new(&block).fieldset
  @block_for_default_fieldset = nil
end
render(object, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
Alias for: dump
resolve_domain_model!() click to toggle source
# File lib/praxis/blueprint.rb, line 248
def self.resolve_domain_model!
  return unless domain_model.is_a?(String)

  @domain_model = domain_model.constantize
end
valid_type?(value) click to toggle source
# File lib/praxis/blueprint.rb, line 163
def self.valid_type?(value)
  value.is_a?(self) || value.is_a?(attribute.type)
end
validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil) click to toggle source
# File lib/praxis/blueprint.rb, line 180
def self.validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
  raise ArgumentError, "Invalid context received (nil) while validating value of type #{name}" if context.nil?

  context = [context] if context.is_a? ::String

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

  value.validate(context)
end
view(name, **_options, &block) click to toggle source
# File lib/praxis/blueprint.rb, line 196
def self.view(name, **_options, &block)
  unless name == :default
    raise "[ERROR] Views are no longer supported. Please use fully expanded fields when rendering.\n" \
          "NOTE that defining the :default view is deprecated, but still temporarily allowed, as an alias to define the default_fieldset.\n" \
          "A view for name #{name} is attempted to be defined in:\n#{Kernel.caller.first}"
  end
  raise 'Cannot define the default fieldset through the default view unless a block is passed' unless block_given?

  puts "[DEPRECATED] default fieldsets should be defined through `default_fieldset` instead of using the view :default block.\n" \
       "A default view is attempted to be defined in:\n#{Kernel.caller.first}"
  default_fieldset(&block)
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/blueprint.rb, line 311
def _cache_key
  object
end
_get_attr(name) click to toggle source

generic semi-private getter used by Renderer

# File lib/praxis/blueprint.rb, line 377
def _get_attr(name)
  send(name)
end
dump(fields: self.class.default_fieldset, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **_opts)
Alias for: render
render(fields: self.class.default_fieldset, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **_opts) click to toggle source

Render the wrapped data with the given fields (or using the default fieldset otherwise)

# File lib/praxis/blueprint.rb, line 316
def render(fields: self.class.default_fieldset, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **_opts)
  # Accept a simple array of fields, and transform it to a 1-level hash with true values
  fields = fields.each_with_object({}) { |field, hash| hash[field] = true } if fields.is_a? Array

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

  context = [context] if context.is_a? ::String

  raise 'validation conflict' if @validating

  @validating = true

  errors = []
  keys_provided = []

  keys_provided = object.contents.keys

  keys_provided.each do |key|
    sub_context = self.class.generate_subcontext(context, key)
    attribute = self.class.attributes[key]

    if object.contents[key].nil?
      errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."] if !Attributor::Attribute.nullable_attribute?(attribute.options) && object.contents.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
      # No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
      # context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
    else
      value = _get_attr(key)
      next if value.respond_to?(:validating) && value.validating # really, it's a thing with sub-attributes

      errors.concat attribute.validate(value, sub_context)
    end
  end

  leftover = self.class.attributes.keys - keys_provided
  leftover.each do |key|
    sub_context = self.class.generate_subcontext(context, key)
    attribute = self.class.attributes[key]

    errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."] if attribute.options[:required]
  end

  self.class.attribute.type.requirements.each do |requirement|
    validation_errors = requirement.validate(keys_provided, context)
    errors.concat(validation_errors) unless validation_errors.empty?
  end
  errors
ensure
  @validating = false
end