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