lib/onsi/model.rb in onsi-0.8.0 vs lib/onsi/model.rb in onsi-1.0.0
- old
+ new
@@ -1,115 +1,246 @@
require 'active_support/concern'
module Onsi
+ ##
+ # The Model helper for create a renderable helper.
+ #
+ # @author Maddie Schipper
+ # @since 1.0.0
+ #
+ # @example
+ # class Person < ApplicationRecord
+ # include Onsi::Model
+ #
+ # api_render(:v1) do
+ # # Passing the name of the attribute only will call that name as a method on
+ # # the instance of the method.
+ # attribute(:first_name)
+ # attribute(:last_name)
+ # # You can give attribute a block and it will be called on the object
+ # # instance. This lets you rename or compute attributes
+ # attribute(:full_name) { "#{first_name} #{last_name}" }
+ #
+ # # Relationship requires a minimum of 2 parameters. The first is the name
+ # # of the relationship in the rendered JSON. The second is the type.
+ # # When fetching the value, Onsi will add `_id` and call that method on the
+ # # object instance. e.g. `team_id` in this case.
+ # relationship(:team, :team)
+ #
+ # # Relationships can take a block that will be called on the object instance
+ # # and the return value will be used as the ID
+ # relationship(:primary_email, :email) { emails.where(primary: true).first.id }
+ # end
+ # end
module Model
- DEFAULT_API_VERSION = :v1
-
extend ActiveSupport::Concern
+ ##
+ # The current default rendered API version.
+ DEFAULT_API_VERSION = :v1
+
+ ##
+ # Defines class methods available on the class.
module ClassMethods
+ ##
+ # Add a version to be rendered.
+ #
+ # @param version [Symbol] The version that will trigger this render block.
+ #
+ # @param block [Block] The block. Called on an instance
+ # of {Onsi::Model::ModelRenderer}
def api_render(version, &block)
api_renderer(version).instance_exec(&block)
end
+ ##
+ # Fetch the {Onsi::Model::ModelRenderer} for the version.
+ #
+ # @param version [Symbol] The version to fetch the renderer for.
+ #
+ # @param for_render [true, false] Specifies if the version should be
+ # required to exist. Should only ever be true when attempting to render
+ # the resource.
+ #
+ # @raise [Onsi::Errors::UnknownVersionError] If the version isn't defined
+ # and the for_render param is true.
def api_renderer(version, for_render: false)
@api_renderer ||= {}
if for_render
raise Errors::UnknownVersionError.new(self, version) if @api_renderer[version].nil?
else
@api_renderer[version] ||= ModelRenderer.new
end
@api_renderer[version]
end
+ end
- class ModelRenderer
- DATE_FORMAT = '%Y-%m-%d'.freeze
- DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
+ ##
+ # The class that holds attributes and relationships for a model's version.
+ #
+ # @note You shouldn't ever have to directly interact with one of
+ # these classes.
+ #
+ # @author Maddie Schipper
+ # @since 1.0.0
+ class ModelRenderer
+ ##
+ # The default date format for a rendered Date. (ISO-8601)
+ DATE_FORMAT = '%Y-%m-%d'.freeze
- def initialize
- @attributes = {}
- @relationships = {}
- @metadata = {}
- end
+ ##
+ # The default date-time format for a rendered Date and Time. (ISO-8601)
+ DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
- def type(name = nil)
- @type = name if name
- @type
- end
+ ##
+ # Create a new ModelRenderer
+ #
+ # @private
+ def initialize
+ @attributes = {}
+ @relationships = {}
+ @metadata = {}
+ end
- def attribute(name, &block)
- @attributes[name.to_sym] = block || name
- end
+ ##
+ # The type name.
+ #
+ # @param name [String, nil] The resource object type name.
+ #
+ # @note Not required. If there is no type, the class name will be used
+ # when rendering the object. (Name is underscored)
+ def type(name = nil)
+ @type = name if name
+ @type
+ end
- def relationship(name, type, &block)
- @relationships[name.to_sym] = { type: type, attr: block || name }
- end
+ ##
+ # Add an attribute to the rendered attributes.
+ #
+ # @param name [String, Symbol, #to_sym] The name of the attribute.
+ # If no block is passed the name will be called on
+ # the {Onsi::Resource#object}
+ #
+ # @param block [Block] The block used to fetch a dynamic attribute.
+ # It will be executed in the context of the {Onsi::Resource#object}
+ #
+ # @example
+ # api_render(:v1) do
+ # attribute(:first_name)
+ # attribute(:last_name)
+ # attribute(:full_name) { "#{first_name} #{last_name}" }
+ #
+ # # ...
+ #
+ # end
+ def attribute(name, &block)
+ @attributes[name.to_sym] = block || name
+ end
- def meta(name, &block)
- @metadata[name.to_sym] = block
- end
+ ##
+ # Add a relationship to the rendered relationships.
+ #
+ # @param name [Symbol, #to_sym] The relationship name.
+ #
+ # @param type [String, #to_s] The relationship type.
+ #
+ # @param block [Block] The block used to fetch a dynamic attribute.
+ # It will be executed in the context of the {Onsi::Resource#object}
+ #
+ # @example
+ # api_render(:v1) do
+ # relationship(:team, :team)
+ #
+ # # ...
+ #
+ # end
+ def relationship(name, type, &block)
+ @relationships[name.to_sym] = { type: type, attr: block || name }
+ end
- def render_attributes(object)
- @attributes.each_with_object({}) do |(key, value), attrs|
- val = value.respond_to?(:call) ? object.instance_exec(&value) : object.send(value)
- attrs[key.to_s] = format_attribute(val)
- end
+ ##
+ # Add a metadata value to the rendered object's meta.
+ #
+ # @param name [#to_sym] The name for the meta value.
+ #
+ # @param block [Block] The block used to fetch the meta value.
+ # It will be executed in the context of the {Onsi::Resource#object}
+ def meta(name, &block)
+ @metadata[name.to_sym] = block
+ end
+
+ ##
+ # Render all attributes
+ #
+ # @private
+ def render_attributes(object)
+ @attributes.each_with_object({}) do |(key, value), attrs|
+ val = value.respond_to?(:call) ? object.instance_exec(&value) : object.send(value)
+ attrs[key.to_s] = format_attribute(val)
end
+ end
- def render_relationships(object)
- @relationships.each_with_object({}) do |(key, value), rels|
- render_relationship_entry(object, key, value, rels)
- end
+ ##
+ # Render all relationships
+ #
+ # @private
+ def render_relationships(object)
+ @relationships.each_with_object({}) do |(key, value), rels|
+ render_relationship_entry(object, key, value, rels)
end
+ end
- def render_metadata(object)
- @metadata.each_with_object({}) do |(key, block), meta|
- meta[key.to_s] = object.instance_exec(&block)
- end
+ ##
+ # Render all metadata
+ #
+ # @private
+ def render_metadata(object)
+ @metadata.each_with_object({}) do |(key, block), meta|
+ meta[key.to_s] = object.instance_exec(&block)
end
+ end
- private
+ private
- def render_relationship_entry(object, key, value, rels)
- attr = value[:attr]
- relationship = get_relationship_value(attr, object)
- data = format_relationship(relationship, value)
- rels[key.to_s] = {
- 'data' => data
- }
- end
+ def render_relationship_entry(object, key, value, rels)
+ attr = value[:attr]
+ relationship = get_relationship_value(attr, object)
+ data = format_relationship(relationship, value)
+ rels[key.to_s] = {
+ 'data' => data
+ }
+ end
- def get_relationship_value(attr, object)
- if attr.respond_to?(:call)
- object.instance_exec(&attr)
- else
- object.send("#{attr}_id")
- end
+ def get_relationship_value(attr, object)
+ if attr.respond_to?(:call)
+ object.instance_exec(&attr)
+ else
+ object.send("#{attr}_id")
end
+ end
- def format_relationship(relationship, value)
- case relationship
- when Array
- relationship.map { |v| { 'type' => value[:type].to_s, 'id' => v.to_s } }
- else
- {
- 'type' => value[:type].to_s,
- 'id' => relationship.to_s
- }
- end
+ def format_relationship(relationship, value)
+ case relationship
+ when Array
+ relationship.map { |v| { 'type' => value[:type].to_s, 'id' => v.to_s } }
+ else
+ {
+ 'type' => value[:type].to_s,
+ 'id' => relationship.to_s
+ }
end
+ end
- def format_attribute(value)
- case value
- when Date
- value.strftime(DATE_FORMAT)
- when DateTime, Time
- value.utc.strftime(DATETIME_FORMAT)
- when String
- value.presence
- else
- value
- end
+ def format_attribute(value)
+ case value
+ when Date
+ value.strftime(DATE_FORMAT)
+ when DateTime, Time
+ value.utc.strftime(DATETIME_FORMAT)
+ when String
+ value.presence
+ else
+ value
end
end
end
end
end