rest_model 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/Gemfile +2 -0
- data/Guardfile +7 -0
- data/README.md +237 -0
- data/Rakefile +2 -0
- data/examples/all.rb +12 -0
- data/examples/embeds_many/invisible.rb +16 -0
- data/examples/embeds_many/simple.rb +13 -0
- data/examples/embeds_many/with_array.rb +10 -0
- data/examples/embeds_many/with_class_name.rb +13 -0
- data/examples/embeds_many/with_fields.rb +9 -0
- data/examples/embeds_many/with_if.rb +16 -0
- data/examples/embeds_many/with_nil_value.rb +12 -0
- data/examples/embeds_many/with_start_key.rb +13 -0
- data/examples/embeds_one/flattened.rb +12 -0
- data/examples/embeds_one/simple.rb +12 -0
- data/examples/embeds_one/with_class_name.rb +12 -0
- data/examples/embeds_one/with_if.rb +16 -0
- data/examples/embeds_one/with_start_key.rb +12 -0
- data/examples/has_many/simple.rb +28 -0
- data/examples/helper.rb +9 -0
- data/examples/initialize/embeds_many.rb +25 -0
- data/examples/initialize/embeds_many_array.rb +9 -0
- data/examples/initialize/embeds_one.rb +19 -0
- data/examples/initialize/simple.rb +8 -0
- data/examples/properties/array_serialization.rb +8 -0
- data/examples/properties/collections.rb +11 -0
- data/examples/properties/simple.rb +8 -0
- data/examples/properties/with_field.rb +8 -0
- data/examples/properties/with_field_path.rb +8 -0
- data/examples/properties/with_id.rb +8 -0
- data/examples/properties/with_if.rb +21 -0
- data/examples/properties/with_key_converter.rb +18 -0
- data/examples/properties/with_two_key_converters.rb +27 -0
- data/examples/properties/with_values.rb +10 -0
- data/examples/summarization/simple.rb +21 -0
- data/examples/to_input/embeds_many.rb +25 -0
- data/examples/to_input/embeds_many_without_key.rb +31 -0
- data/examples/to_input/embeds_one.rb +13 -0
- data/examples/to_input/embeds_one_without_key.rb +23 -0
- data/examples/to_input/flattened.rb +12 -0
- data/examples/to_input/serializables.rb +19 -0
- data/examples/to_input/simple.rb +8 -0
- data/examples/to_input/with_field.rb +8 -0
- data/examples/to_input/with_field_path.rb +8 -0
- data/examples/to_input/with_fields.rb +8 -0
- data/examples/to_input/without_key.rb +11 -0
- data/examples/to_input/without_nil.rb +11 -0
- data/lib/rest_model/configuration.rb +45 -0
- data/lib/rest_model/key/association.rb +42 -0
- data/lib/rest_model/key/builder.rb +30 -0
- data/lib/rest_model/key/embeddable/builder.rb +19 -0
- data/lib/rest_model/key/embeddable/response.rb +25 -0
- data/lib/rest_model/key/embeddable/retriever.rb +20 -0
- data/lib/rest_model/key/embeddable/sender.rb +41 -0
- data/lib/rest_model/key/embeddable.rb +29 -0
- data/lib/rest_model/key/href/response.rb +10 -0
- data/lib/rest_model/key/href.rb +10 -0
- data/lib/rest_model/key/property/builder.rb +27 -0
- data/lib/rest_model/key/property/response.rb +9 -0
- data/lib/rest_model/key/property/retriever.rb +24 -0
- data/lib/rest_model/key/property/sender.rb +41 -0
- data/lib/rest_model/key/property.rb +22 -0
- data/lib/rest_model/key/relation/builder.rb +24 -0
- data/lib/rest_model/key/relation/response.rb +38 -0
- data/lib/rest_model/key/relation.rb +23 -0
- data/lib/rest_model/key.rb +44 -0
- data/lib/rest_model/response.rb +42 -0
- data/lib/rest_model/serialization/boolean.rb +48 -0
- data/lib/rest_model/serialization/date.rb +19 -0
- data/lib/rest_model/serialization/enumerable.rb +15 -0
- data/lib/rest_model/serialization/float.rb +17 -0
- data/lib/rest_model/serialization/integer.rb +17 -0
- data/lib/rest_model/serialization/string.rb +13 -0
- data/lib/rest_model/source/path.rb +25 -0
- data/lib/rest_model/source/retriever.rb +48 -0
- data/lib/rest_model/source/sender.rb +38 -0
- data/lib/rest_model/version.rb +3 -0
- data/lib/rest_model.rb +101 -0
- data/rest_model.gemspec +26 -0
- data/spec/.DS_Store +0 -0
- data/spec/integration/embeds_many_spec.rb +57 -0
- data/spec/integration/embeds_one_spec.rb +41 -0
- data/spec/integration/has_many_spec.rb +27 -0
- data/spec/integration/property_spec.rb +69 -0
- data/spec/integration/summarization_spec.rb +12 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/examples.rb +29 -0
- data/spec/support/out.rb +17 -0
- data/spec/support/shared_examples.rb +20 -0
- data/spec/unit/configuration_spec.rb +24 -0
- data/spec/unit/key/association_spec.rb +69 -0
- data/spec/unit/key/embeddable/builder_spec.rb +26 -0
- data/spec/unit/key/embeddable/response_spec.rb +55 -0
- data/spec/unit/key/embeddable/retriever_spec.rb +38 -0
- data/spec/unit/key/embeddable_spec.rb +7 -0
- data/spec/unit/key/property/builder_spec.rb +154 -0
- data/spec/unit/key/property/response_spec.rb +22 -0
- data/spec/unit/key/property/retriever_spec.rb +31 -0
- data/spec/unit/key/property_spec.rb +39 -0
- data/spec/unit/key/relation/builder_spec.rb +53 -0
- data/spec/unit/key/relation/response_spec.rb +105 -0
- data/spec/unit/key/relation_spec.rb +71 -0
- data/spec/unit/key_spec.rb +101 -0
- data/spec/unit/response_spec.rb +44 -0
- data/spec/unit/rest_model_spec.rb +46 -0
- data/spec/unit/serialization/boolean_spec.rb +25 -0
- data/spec/unit/serialization/date_spec.rb +13 -0
- data/spec/unit/serialization/float_spec.rb +13 -0
- data/spec/unit/serialization/integer_spec.rb +13 -0
- data/spec/unit/serialization/string_spec.rb +11 -0
- data/spec/unit/source/path_spec.rb +62 -0
- data/spec/unit/source/retriever_spec.rb +85 -0
- metadata +260 -0
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              class Relation
         | 
| 3 | 
            +
                module Builder
         | 
| 4 | 
            +
                  def relation(name, options = {})
         | 
| 5 | 
            +
                    key Relation.new(name, options)
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def has_one(name, options = {})
         | 
| 9 | 
            +
                    options.merge!(many: false, has: true)
         | 
| 10 | 
            +
                    relation(name, options)
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def has_many(name, options = {})
         | 
| 14 | 
            +
                    options.merge!(many: true, has: true)
         | 
| 15 | 
            +
                    relation(name, options)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def belongs_to(name, options = {})
         | 
| 19 | 
            +
                    options.merge!(many: false, has: false)
         | 
| 20 | 
            +
                    relation(name, options)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              class Relation
         | 
| 3 | 
            +
                module Response
         | 
| 4 | 
            +
                  def to_resource(parent)
         | 
| 5 | 
            +
                    included = parent.__send__(name)
         | 
| 6 | 
            +
                    return {} unless visible?(parent) and included
         | 
| 7 | 
            +
                    {name => resource_from_included(included)}
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def to_relation(parent)
         | 
| 11 | 
            +
                    {rel: name, href: href(parent)}
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  private
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def relation_name(parent)
         | 
| 17 | 
            +
                    relation = parent.class.relations.find do |key|
         | 
| 18 | 
            +
                      key.resource_class == self.resource_class
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                    relation.name or fail
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def resource_from_included(included)
         | 
| 24 | 
            +
                    options = {root: false}
         | 
| 25 | 
            +
                    one? ?
         | 
| 26 | 
            +
                      included.resource(options)
         | 
| 27 | 
            +
                    : included.map {|r| r.resource(options)}
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def href(parent)
         | 
| 31 | 
            +
                    id_key = parent.class.id_key.name
         | 
| 32 | 
            +
                    has? ?
         | 
| 33 | 
            +
                      "#{RestModel::Configuration.host}/#{parent.class.resource_name}/#{parent.send(id_key)}/#{relation_name(parent)}"
         | 
| 34 | 
            +
                    : "#{RestModel::Configuration.host}/#{relation_name(parent).to_s.pluralize}/#{parent.send(id_key)}/#{parent.class.resource_name}"
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              class Relation < Association
         | 
| 3 | 
            +
                autoload :Response, "rest_model/key/relation/response"
         | 
| 4 | 
            +
                include Response
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def initialize(name, options = {})
         | 
| 7 | 
            +
                  super
         | 
| 8 | 
            +
                  @has = options.fetch(:has, false)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def has?
         | 
| 12 | 
            +
                  @has
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def belongs?
         | 
| 16 | 
            +
                  !has?
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def parse(value, resource = nil)
         | 
| 20 | 
            +
                  nil
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              class Key
         | 
| 3 | 
            +
                attr_accessor :name, :model, :options, :summarize
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(name, options = {})
         | 
| 6 | 
            +
                  @name    = name
         | 
| 7 | 
            +
                  @options = options
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def convert_input_keys
         | 
| 11 | 
            +
                  options.fetch(:convert_input_keys, model.try(:convert_input_keys) ||
         | 
| 12 | 
            +
                                RestModel::Configuration.convert_input_keys)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def source_path
         | 
| 16 | 
            +
                  return @source_path.clone if @source_path
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  @source_path = Source::Path.resolve(options, convert_input_keys)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  if @source_path.empty? and !root_path?
         | 
| 21 | 
            +
                    @source_path = convert_input_keys.call([name])
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  @source_path.clone
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def root_path?
         | 
| 28 | 
            +
                  path_definition = options.slice(:start_key, :field)
         | 
| 29 | 
            +
                  path_definition.any? and path_definition.first[1].empty?
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def present?(resource)
         | 
| 33 | 
            +
                  !options[:if] ? true : resource.instance_eval(&options[:if])
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def visible?(resource)
         | 
| 37 | 
            +
                  !present?(resource) ? false : case visible = options[:visible]
         | 
| 38 | 
            +
                                                when nil  then true
         | 
| 39 | 
            +
                                                when Proc then resource.instance_eval(&visible)
         | 
| 40 | 
            +
                                                else visible
         | 
| 41 | 
            +
                                                end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Response
         | 
| 3 | 
            +
                extend ActiveSupport::Concern
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def resource(options = {})
         | 
| 6 | 
            +
                  root = options.fetch(:root, true)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  {}.tap do |resource|
         | 
| 9 | 
            +
                    resource_keys(options).inject(resource) do |buffer, key|
         | 
| 10 | 
            +
                      buffer.merge!(key.to_resource(self))
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    if root and self.class.relations.any? and !options[:summarize]
         | 
| 14 | 
            +
                      resource.merge!({link: link})
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def link
         | 
| 20 | 
            +
                  self.class.relations.map {|key| key.to_relation(self)}
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def resource_keys(options)
         | 
| 24 | 
            +
                  summarize?(options) ? self.class.summarized_keys + [Href.new]
         | 
| 25 | 
            +
                                      : self.class.keys
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def summarize?(options)
         | 
| 29 | 
            +
                  options[:summarize] and self.class.summarized_keys.any?
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                module ClassMethods
         | 
| 33 | 
            +
                  def normalize(model, options = {})
         | 
| 34 | 
            +
                    model.kind_of?(Enumerable) ?
         | 
| 35 | 
            +
                      {entries: model.map {|m| m.resource(options.merge(summarize: true))}}
         | 
| 36 | 
            +
                     : model.resource(options)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  alias :resources :normalize
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Serialization
         | 
| 3 | 
            +
                class Boolean
         | 
| 4 | 
            +
                  MAPPINGS  = {
         | 
| 5 | 
            +
                    true    => true,
         | 
| 6 | 
            +
                    "true"  => true,
         | 
| 7 | 
            +
                    "TRUE"  => true,
         | 
| 8 | 
            +
                    "1"     => true,
         | 
| 9 | 
            +
                    1       => true,
         | 
| 10 | 
            +
                    1.0     => true,
         | 
| 11 | 
            +
                    "x"     => true,
         | 
| 12 | 
            +
                    "X"     => true,
         | 
| 13 | 
            +
                    "t"     => true,
         | 
| 14 | 
            +
                    "T"     => true,
         | 
| 15 | 
            +
                    false   => false,
         | 
| 16 | 
            +
                    "false" => false,
         | 
| 17 | 
            +
                    "FALSE" => false,
         | 
| 18 | 
            +
                    "0"     => false,
         | 
| 19 | 
            +
                    0       => false,
         | 
| 20 | 
            +
                    0.0     => false,
         | 
| 21 | 
            +
                    ""      => false,
         | 
| 22 | 
            +
                    " "     => false,
         | 
| 23 | 
            +
                    "f"     => false,
         | 
| 24 | 
            +
                    "F"     => false,
         | 
| 25 | 
            +
                    nil     => false
         | 
| 26 | 
            +
                  }.freeze
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def self.serialize(value)
         | 
| 29 | 
            +
                    MAPPINGS[value].tap do |bool|
         | 
| 30 | 
            +
                      fail "value not serializable: #{value}" if bool.nil?
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def self.desserialize(value)
         | 
| 35 | 
            +
                    fail unless boolean?(value)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    value ? RestModel.configuration.true_value
         | 
| 38 | 
            +
                          : RestModel.configuration.false_value
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  private
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def self.boolean?(value)
         | 
| 44 | 
            +
                    !!value == value
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Serialization
         | 
| 3 | 
            +
                class Date
         | 
| 4 | 
            +
                  def self.serialize(value)
         | 
| 5 | 
            +
                    ::Date.parse value
         | 
| 6 | 
            +
                  rescue ArgumentError
         | 
| 7 | 
            +
                    raise "value not serializable: #{value}"
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.desserialize(value)
         | 
| 11 | 
            +
                    date = value.kind_of?(::Date) ? value : ::Date.parse(value)
         | 
| 12 | 
            +
                    format = RestModel.configuration.date_format
         | 
| 13 | 
            +
                    format ? date.strftime(format) : date.to_s
         | 
| 14 | 
            +
                  rescue ArgumentError
         | 
| 15 | 
            +
                    raise "value not desserializable: #{value}"
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Serialization
         | 
| 3 | 
            +
                class Enumerable
         | 
| 4 | 
            +
                  def self.serialize(value)
         | 
| 5 | 
            +
                    fail "value not serializable: #{value}" unless value.kind_of?(::Enumerable)
         | 
| 6 | 
            +
                    value
         | 
| 7 | 
            +
                  end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def self.desserialize(value)
         | 
| 10 | 
            +
                    fail "value not desserializable: #{value}" unless value.kind_of?(::Enumerable)
         | 
| 11 | 
            +
                    value
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Serialization
         | 
| 3 | 
            +
                class Float
         | 
| 4 | 
            +
                  def self.serialize(value)
         | 
| 5 | 
            +
                    Float(value)
         | 
| 6 | 
            +
                  rescue ArgumentError
         | 
| 7 | 
            +
                    raise "value not serializable: #{value}"
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.desserialize(value)
         | 
| 11 | 
            +
                    Float(value)
         | 
| 12 | 
            +
                  rescue ArgumentError
         | 
| 13 | 
            +
                    raise "value not desserializable: #{value}"
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Serialization
         | 
| 3 | 
            +
                class Integer
         | 
| 4 | 
            +
                  def self.serialize(value)
         | 
| 5 | 
            +
                    Integer(value)
         | 
| 6 | 
            +
                  rescue ArgumentError
         | 
| 7 | 
            +
                    raise "value not serializable: #{value}"
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.desserialize(value)
         | 
| 11 | 
            +
                    Integer(value)
         | 
| 12 | 
            +
                  rescue ArgumentError
         | 
| 13 | 
            +
                    raise "value not desserializable: #{value}"
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Source
         | 
| 3 | 
            +
                module Path
         | 
| 4 | 
            +
                  def self.resolve(options, convert_input_keys = Key::Converter::DefaultHandler)
         | 
| 5 | 
            +
                    convert_input_keys.call keys_for_path(custom_path(options))
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  private
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.custom_path(options)
         | 
| 11 | 
            +
                    options.slice(:start_key, :field)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def self.keys_for_path(path)
         | 
| 15 | 
            +
                    return [] if path.empty?
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    if path.values.first.kind_of?(Symbol)
         | 
| 18 | 
            +
                      [path.values.first]
         | 
| 19 | 
            +
                    else
         | 
| 20 | 
            +
                      path.values.first.to_s.split('.')
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Source
         | 
| 3 | 
            +
                module Retriever
         | 
| 4 | 
            +
                  def from_source!(input, options = {})
         | 
| 5 | 
            +
                    from_source(input, options.merge(fail: true))
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def from_source(input, options = {})
         | 
| 9 | 
            +
                    entries = prepare_entries(input, options)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    if entries.any? {|e| !e.kind_of?(Hash)}
         | 
| 12 | 
            +
                      fail "invalid input" if options[:fail]
         | 
| 13 | 
            +
                      return []
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    entries.collect do |item|
         | 
| 17 | 
            +
                      handle_item(item.with_indifferent_access, options)
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  alias :parse! :from_source!
         | 
| 22 | 
            +
                  alias :parse  :from_source
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  private
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def prepare_entries(input, options)
         | 
| 27 | 
            +
                    path = Source::Path.resolve(options, RestModel::Configuration.convert_input_keys)
         | 
| 28 | 
            +
                    Array.wrap digg(input, path)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def handle_item(item, options)
         | 
| 32 | 
            +
                    self.new.tap do |resource|
         | 
| 33 | 
            +
                      keys.each do |key|
         | 
| 34 | 
            +
                        begin
         | 
| 35 | 
            +
                          resource.__send__("#{key.name}=", key.from_source(item, resource)) if key.present?(resource)
         | 
| 36 | 
            +
                        rescue => exception
         | 
| 37 | 
            +
                          raise exception if options[:fail]
         | 
| 38 | 
            +
                        end
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def digg(input, path)
         | 
| 44 | 
            +
                    path.inject(input) {|buffer, key| buffer = buffer[key]}
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            class RestModel
         | 
| 2 | 
            +
              module Source
         | 
| 3 | 
            +
                module Sender
         | 
| 4 | 
            +
                  def to_source!(options = {})
         | 
| 5 | 
            +
                    to_source(options.merge(fail: true))
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def to_source(options = {})
         | 
| 9 | 
            +
                    source = {}
         | 
| 10 | 
            +
                    root_options = {without_nil: options[:without_nil], fail: options[:fail]}
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    keys_to_source(options).each do |key|
         | 
| 13 | 
            +
                      value = __send__(key.name)
         | 
| 14 | 
            +
                      key_options = options.fetch(key.name, {}).merge(root_options)
         | 
| 15 | 
            +
                      source.merge! key.to_source!(value, self, key_options)
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    source.with_indifferent_access
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  alias :to_input! :to_source!
         | 
| 22 | 
            +
                  alias :to_input  :to_source
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  private
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def keys_to_source(options)
         | 
| 27 | 
            +
                    self.class.keys.clone.tap do |keys|
         | 
| 28 | 
            +
                      keys.reject! {|k| k.kind_of?(Relation)}
         | 
| 29 | 
            +
                      keys.reject! {|k| Array(options[:without]).include?(k.name)}
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      keys.reject! do |key|
         | 
| 32 | 
            +
                        __send__(key.name).nil? and options[:without_nil]
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
    
        data/lib/rest_model.rb
    ADDED
    
    | @@ -0,0 +1,101 @@ | |
| 1 | 
            +
            require "date"
         | 
| 2 | 
            +
            require "active_support/inflector"
         | 
| 3 | 
            +
            require "active_support/concern"
         | 
| 4 | 
            +
            require "active_support/core_ext/class/attribute_accessors"
         | 
| 5 | 
            +
            require "active_support/core_ext/object/try"
         | 
| 6 | 
            +
            require "active_support/core_ext/array/wrap"
         | 
| 7 | 
            +
            require "active_support/core_ext/hash/slice"
         | 
| 8 | 
            +
            require "active_support/core_ext/hash/indifferent_access"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            require "rest_model/source/path"
         | 
| 11 | 
            +
            require "rest_model/source/retriever"
         | 
| 12 | 
            +
            require "rest_model/source/sender"
         | 
| 13 | 
            +
            require "rest_model/response"
         | 
| 14 | 
            +
            require "rest_model/serialization/boolean"
         | 
| 15 | 
            +
            require "rest_model/serialization/date"
         | 
| 16 | 
            +
            require "rest_model/serialization/enumerable"
         | 
| 17 | 
            +
            require "rest_model/serialization/float"
         | 
| 18 | 
            +
            require "rest_model/serialization/integer"
         | 
| 19 | 
            +
            require "rest_model/serialization/string"
         | 
| 20 | 
            +
            require "rest_model/key"
         | 
| 21 | 
            +
            require "rest_model/key/property"
         | 
| 22 | 
            +
            require "rest_model/key/property/builder"
         | 
| 23 | 
            +
            require "rest_model/key/association"
         | 
| 24 | 
            +
            require "rest_model/key/relation"
         | 
| 25 | 
            +
            require "rest_model/key/relation/builder"
         | 
| 26 | 
            +
            require "rest_model/key/embeddable"
         | 
| 27 | 
            +
            require "rest_model/key/embeddable/builder"
         | 
| 28 | 
            +
            require "rest_model/key/href"
         | 
| 29 | 
            +
            require "rest_model/key/builder"
         | 
| 30 | 
            +
            require "rest_model/configuration"
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            class RestModel
         | 
| 33 | 
            +
              extend  Key::Builder
         | 
| 34 | 
            +
              extend  Source::Retriever
         | 
| 35 | 
            +
              include Source::Sender
         | 
| 36 | 
            +
              include Response
         | 
| 37 | 
            +
              include Serialization
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              cattr_accessor :id_key
         | 
| 40 | 
            +
              cattr_writer   :resource_name
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              def initialize(attrs = {})
         | 
| 43 | 
            +
                return if attrs.nil? or attrs.empty?
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                attrs = attrs.with_indifferent_access
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                self.class.keys.each do |key|
         | 
| 48 | 
            +
                  __send__("#{key.name}=", key.from_hash(attrs[key.name])) if key.present?(self)
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def update_attributes(attrs = {})
         | 
| 53 | 
            +
                return if attrs.nil? or attrs.empty?
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                attrs = attrs.with_indifferent_access
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                self.class.keys.each do |key|
         | 
| 58 | 
            +
                  value = attrs[key.name]
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  if value and key.present?(self)
         | 
| 61 | 
            +
                    __send__("#{key.name}=", key.from_hash(value, __send__(key.name)))
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                self
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              def resource_id
         | 
| 69 | 
            +
                __send__(id_key.name)
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              def self.keys
         | 
| 73 | 
            +
                @keys ||= []
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
              def self.summarized_keys
         | 
| 77 | 
            +
                @summarized_keys ||= []
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              def self.relations
         | 
| 81 | 
            +
                @keys.find_all {|k| k.kind_of?(Relation)}
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              def self.resource_name(custom_name = nil)
         | 
| 85 | 
            +
                @resource_name ||= custom_name or to_s.demodulize.tableize
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              def self.convert_input_keys(converter = nil)
         | 
| 89 | 
            +
                @convert_input_keys = converter if converter
         | 
| 90 | 
            +
                @convert_input_keys
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
              def self.method_added(method_name)
         | 
| 94 | 
            +
                return unless not_allowed_names.include?(method_name.to_s)
         | 
| 95 | 
            +
                puts "warning: redefining '#{method_name}' may cause serious problems"
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
              def self.not_allowed_names
         | 
| 99 | 
            +
                %w(resource_id resource link)
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
            end
         | 
    
        data/rest_model.gemspec
    ADDED
    
    | @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            # -*- encoding: utf-8 -*-
         | 
| 2 | 
            +
            $:.push File.expand_path("../lib", __FILE__)
         | 
| 3 | 
            +
            require "rest_model/version"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Gem::Specification.new do |s|
         | 
| 6 | 
            +
              s.name        = "rest_model"
         | 
| 7 | 
            +
              s.version     = RestModel::VERSION
         | 
| 8 | 
            +
              s.platform    = Gem::Platform::RUBY
         | 
| 9 | 
            +
              s.authors     = ["Victor Rodrigues",           "Vinicius Higa",                "William Yokoi"]
         | 
| 10 | 
            +
              s.email       = ["victorcrodrigues@gmail.com", "vinicius.higa@locaweb.com.br", "thekina@gmail.com"]
         | 
| 11 | 
            +
              s.homepage    = "http://github.com/rodrigues/rest_model"
         | 
| 12 | 
            +
              s.summary     = %q{}
         | 
| 13 | 
            +
              s.description = %q{}
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              s.files         = `git ls-files`.split("\n")
         | 
| 16 | 
            +
              s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
         | 
| 17 | 
            +
              s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
         | 
| 18 | 
            +
              s.require_paths = ["lib"]
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              s.add_dependency "activesupport", "~> 3.0"
         | 
| 21 | 
            +
              s.add_dependency "i18n",          ">= 0.5"
         | 
| 22 | 
            +
              s.add_development_dependency "rspec",       "~> 2.6"
         | 
| 23 | 
            +
              s.add_development_dependency "guard",       "~> 0.5"
         | 
| 24 | 
            +
              s.add_development_dependency "guard-rspec", "~> 0.4"
         | 
| 25 | 
            +
              s.add_development_dependency "growl",       "~> 1.0"
         | 
| 26 | 
            +
            end
         | 
    
        data/spec/.DS_Store
    ADDED
    
    | Binary file | 
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            # encoding: utf-8
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "spec_helper"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe_example "embeds_many/simple" do
         | 
| 6 | 
            +
              it "parses first embedded item" do
         | 
| 7 | 
            +
                root.items[0].id.should == 2000
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              it "parses second embedded item" do
         | 
| 11 | 
            +
                root.items[1].id.should == 2001
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            describe_example "embeds_many/invisible" do
         | 
| 16 | 
            +
              it "parses invisible items correctly" do
         | 
| 17 | 
            +
                root.locale.should == "pt-BR"
         | 
| 18 | 
            +
                root.names.should == {"en"=>"Woot", "pt-BR"=>"Úia", "es"=>"Me gusta"}
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              it "doesn't show embedded items in resource" do
         | 
| 22 | 
            +
                root.resource.should_not have_key("locale")
         | 
| 23 | 
            +
                root.resource.should_not have_key("names")
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            describe_example "embeds_many/with_array" do
         | 
| 28 | 
            +
              it ""
         | 
| 29 | 
            +
            end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            describe_example "embeds_many/with_class_name" do
         | 
| 32 | 
            +
              it "parses embedded item" do
         | 
| 33 | 
            +
                root.items[0].id.should == 2000
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            describe_example "embeds_many/with_if" do
         | 
| 38 | 
            +
              context "when an embeddable has a conditional proc (:if)" do
         | 
| 39 | 
            +
                context "and it evaluates to true" do
         | 
| 40 | 
            +
                  it "parses embedded item" do
         | 
| 41 | 
            +
                    root_with_items.items.first.id.should == "2000"
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                context "and it evaluates to false" do
         | 
| 46 | 
            +
                  it "doesn't parse embedded item" do
         | 
| 47 | 
            +
                    root_without_items.items.should_not be
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            describe_example "embeds_many/with_start_key" do
         | 
| 54 | 
            +
              it "parses embedded items" do
         | 
| 55 | 
            +
                root.items.first.id.should == "2000"
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe_example "embeds_one/simple" do
         | 
| 4 | 
            +
              it 'parses embedded item' do
         | 
| 5 | 
            +
                root.item.id.should == "2000"
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
            end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            describe_example "embeds_one/with_class_name" do
         | 
| 10 | 
            +
              context 'when a different class name is used for embeddable' do
         | 
| 11 | 
            +
                it 'parses embedded item' do
         | 
| 12 | 
            +
                  root.item.id.should == "2000"
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            describe_example "embeds_one/with_if" do
         | 
| 18 | 
            +
              context 'when an embeddable has a conditional proc (:if)' do
         | 
| 19 | 
            +
                context 'and it evaluates to true' do
         | 
| 20 | 
            +
                  it 'parses embedded item' do
         | 
| 21 | 
            +
                    root_with_item.id.should == "1"
         | 
| 22 | 
            +
                    root_with_item.item.id.should == "2000"
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                context 'and it evaluates to false' do
         | 
| 27 | 
            +
                  it "doesn't parse embedded item" do
         | 
| 28 | 
            +
                    root_without_item.id.should == "100"
         | 
| 29 | 
            +
                    root_without_item.item.should_not be
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            describe_example "embeds_one/with_start_key" do
         | 
| 36 | 
            +
              context 'when there is a start key to parse input' do
         | 
| 37 | 
            +
                it 'parses embedded item' do
         | 
| 38 | 
            +
                  root.item.id.should == "2000"
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe_example "has_many/simple" do
         | 
| 4 | 
            +
              it 'parses properly' do
         | 
| 5 | 
            +
                root.id.should == 123
         | 
| 6 | 
            +
                root.login.should == 'jackiechan2010'
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              describe "#resource" do
         | 
| 10 | 
            +
                subject {root.resource}
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                it 'has a link' do
         | 
| 13 | 
            +
                  subject[:link].should be_an(Array)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                [:services, :billing, :devops].each do |rel|
         | 
| 17 | 
            +
                  it "has a #{rel} relation" do
         | 
| 18 | 
            +
                    subject[:link].any? {|l| l[:rel] == rel}.should be_true
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  it "has a href for #{rel} relation" do
         | 
| 22 | 
            +
                    link = subject[:link].find {|l| l[:rel] == rel}
         | 
| 23 | 
            +
                    link[:href].should =~ %r{/customers/123/#{rel}}
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         |