Code Monkey home page Code Monkey logo

cistern's Introduction

Cistern

Join the chat at https://gitter.im/lanej/cistern Build Status Dependencies Gem Version Code Climate

Cistern helps you consistently build your API clients and faciliates building mock support.

Usage

Client

This represents the remote service that you are wrapping. It defines the client's namespace and initialization parameters.

Client initialization parameters are enumerated by requires and recognizes. Parameters defined using recognizes are optional.

# lib/blog.rb
class Blog
  include Cistern::Client

  requires :hmac_id, :hmac_secret
  recognizes :url
end

# Acceptable
Blog.new(hmac_id: "1", hmac_secret: "2")                            # Blog::Real
Blog.new(hmac_id: "1", hmac_secret: "2", url: "http://example.org") # Blog::Real

# ArgumentError
Blog.new(hmac_id: "1", url: "http://example.org")
Blog.new(hmac_id: "1")

Cistern will define for two namespaced classes, Blog::Mock and Blog::Real. Create the corresponding files and initialzers for your new service.

# lib/blog/real.rb
class Blog::Real
  attr_reader :url, :connection

  def initialize(attributes)
    @hmac_id, @hmac_secret = attributes.values_at(:hmac_id, :hmac_secret)
    @url = attributes[:url] || 'http://blog.example.org'
    @connection = Faraday.new(url)
  end
end
# lib/blog/mock.rb
class Blog::Mock
  attr_reader :url

  def initialize(attributes)
    @url = attributes[:url]
  end
end

Mocking

Cistern strongly encourages you to generate mock support for your service. Mocking can be enabled using mock!.

Blog.mocking?          # falsey
real = Blog.new        # Blog::Real
Blog.mock!
Blog.mocking?          # true
fake = Blog.new        # Blog::Mock
Blog.unmock!
Blog.mocking?          # false
real.is_a?(Blog::Real) # true
fake.is_a?(Blog::Mock) # true

Requests

Requests are defined by subclassing #{service}::Request.

  • cistern represents the associated Blog instance.
  • #call represents the primary entrypoint. Invoked when calling client#{request_method}.
  • #dispatch determines which method to call. (#mock or #real)

For example:

class Blog::UpdatePost
  include Blog::Request

  def real(id, parameters)
    cistern.connection.patch("/post/#{id}", parameters)
  end

  def mock(id, parameters)
    post = cistern.data[:posts].fetch(id)

    post.merge!(stringify_keys(parameters))

    response(post: post)
  end
end

However, if you want to add some preprocessing to your request's arguments override #call and call #dispatch. You can also alter the response method's signatures based on the arguments provided to #dispatch.

class Blog::UpdatePost
  include Blog::Request

  attr_reader :parameters

  def call(post_id, parameters)
    @parameters = stringify_keys(parameters)
    dispatch(Integer(post_id))
  end

  def real(id)
    cistern.connection.patch("/post/#{id}", parameters)
  end

  def mock(id)
    post = cistern.data[:posts].fetch(id)

    post.merge!(parameters)

    response(post: post)
  end
end

The #cistern_method function allows you to specify the name of the generated method.

class Blog::GetPosts
  include Blog::Request

  cistern_method :get_all_the_posts

  def real(params)
    "all the posts"
  end
end

Blog.new.respond_to?(:get_posts) # false
Blog.new.get_all_the_posts       # "all the posts"

All declared requests can be listed via Cistern::Client#requests.

Blog.requests # => [Blog::GetPosts, Blog::GetPost]

Models

  • cistern represents the associated Blog::Real or Blog::Mock instance.
  • collection represents the related collection.
  • new_record? checks if identity is present
  • requires(*requirements) throws ArgumentError if an attribute matching a requirement isn't set
  • requires_one(*requirements) throws ArgumentError if no attribute matching requirement is set
  • merge_attributes(attributes) sets attributes for the current model instance
  • dirty_attributes represents attributes changed since the last merge_attributes. This is useful for using update

Attributes

Cistern attributes are designed to make your model flexible and developer friendly.

  • attribute :post_id adds an accessor to the model.

     attribute :post_id
    
     model.post_id #=> nil
     model.post_id = 1 #=> 1
     model.post_id #=> 1
     model.attributes #=> {'post_id' => 1 }
     model.dirty_attributes #=> {'post_id' => 1 }
  • identity represents the name of the model's unique identifier. As this is not always available, it is not required.

     identity :name

    creates an attribute called name that is aliased to identity.

     model.name = 'michelle'
    
     model.identity   #=> 'michelle'
     model.name       #=> 'michelle'
     model.attributes #=> {  'name' => 'michelle' }
  • :aliases or :alias allows a attribute key to be different then a response key.

     attribute :post_id, alias: "post"

    allows

     model.merge_attributes("post" => 1)
     model.post_id #=> 1
  • :type automatically casts the attribute do the specified type. Supported types: array, boolean, date, float, integer, string, time.

     attribute :private_ips, type: :array
    
     model.merge_attributes("private_ips" => 2)
     model.private_ips #=> [2]
  • :squash traverses nested hashes for a key.

     attribute :post_id, aliases: "post", squash: "id"
    
     model.merge_attributes("post" => {"id" => 3})
     model.post_id #=> 3

Persistence

  • save is used to persist the model into the remote service. save is responsible for determining if the operation is an update to an existing resource or a new resource.
  • reload is used to grab the latest data and merge it into the model. reload uses collection.get(identity) by default.
  • update(attrs) is a merge_attributes and a save. When calling update, dirty_attributes can be used to persist only what has changed locally.

For example:

class Blog::Post
  include Blog::Model
  identity :id, type: :integer

  attribute :body
  attribute :author_id, aliases: "author",  squash: "id"
  attribute :deleted_at, type: :time

  def destroy
    requires :identity

    data = cistern.destroy_post(params).body['post']
  end

  def save
    requires :author_id

    response = if new_record?
                 cistern.create_post(attributes)
               else
                 cistern.update_post(dirty_attributes)
               end

    merge_attributes(response.body['post'])
  end
end

Usage:

create

blog.posts.create(author_id: 1, body: 'text')

is equal to

post = blog.posts.new(author_id: 1, body: 'text')
post.save

update

post = blog.posts.get(1)
post.update(author_id: 1) #=> calls #save with #dirty_attributes == { 'author_id' => 1 }
post.author_id #=> 1

Singular

Singular resources do not have an associated collection and the model contains the get andsave methods.

For instance:

class Blog::PostData
  include Blog::Singular

  attribute :post_id, type: :integer
  attribute :upvotes, type: :integer
  attribute :views, type: :integer
  attribute :rating, type: :float

  def get
    response = cistern.get_post_data(post_id)
    merge_attributes(response.body['data'])
  end
  
  def save
    response = cistern.update_post_data(post_id, dirty_attributes)
    merge_attributes(response.data['data'])
  end
end

Singular resources often hang off of other models or collections.

class Blog::Post
  include Cistern::Model

  identity :id, type: :integer

  def data
    cistern.post_data(post_id: identity).load
  end
end

They are special cases of Models and have similar interfaces.

post.data.views #=> nil
post.data.update(views: 3)
post.data.views #=> 3

Collection

  • model tells Cistern which resource class this collection represents.
  • cistern is the associated Blog::Real or Blog::Mock instance
  • attribute specifications on collections are allowed. use merge_attributes
  • load consumes an Array of data and constructs matching model instances
class Blog::Posts
  include Blog::Collection

  attribute :count, type: :integer

  model Blog::Post

  def all(params = {})
    response = cistern.get_posts(params)

    data = response.body

    load(data["posts"])    # store post records in collection
    merge_attributes(data) # store any other attributes of the response on the collection
  end

  def discover(author_id, options={})
    params = {
      "author_id" => author_id,
    }
    params.merge!("topic" => options[:topic]) if options.key?(:topic)

    cistern.blogs.new(cistern.discover_blog(params).body["blog"])
  end

  def get(id)
    data = cistern.get_post(id).body["post"]

    new(data) if data
  end
end

Associations

Associations allow the use of a resource's attributes to reference other resources. They act as lazy loaded attributes and push any loaded data into the resource's attributes.

There are two types of associations available.

  • belongs_to references a specific resource and defines a reader.
  • has_many references a collection of resources and defines a reader / writer.
class Blog::Tag
  include Blog::Model

  identity :id
  attribute :author_id

  has_many :posts -> { cistern.posts(tag_id: identity) }
  belongs_to :creator -> { cistern.authors.get(author_id) }
end

Relationships store the collection's attributes within the resources' attributes on write / load.

tag = blog.tags.get('ruby')
tag.posts = blog.posts.load({'id' => 1, 'author_id' => '2'}, {'id' => 2, 'author_id' => 3})
tag.attributes[:posts] #=> {'id' => 1, 'author_id' => '2'}, {'id' => 2, 'author_id' => 3}

tag.creator = blogs.author.get(name: 'phil')
tag.attributes[:creator] #=> { 'id' => 2, 'name' => 'phil' }

Foreign keys can be updated by overriding the association writer.

Blog::Tag.class_eval do
  def creator=(creator)
    super
    self.author_id = attributes[:creator][:id]
  end
end

tag = blog.tags.get('ruby')
tag.author_id = 4
tag.creator = blogs.author.get(name: 'phil') #=> #<Blog::Author id=2 name='phil'>
tag.author_id #=> 2

Data

A uniform interface for mock data is mixed into the Mock class by default.

Blog.mock!
client = Blog.new # Blog::Mock
client.data       # Cistern::Data::Hash
client.data["posts"] += ["x"] # ["x"]

Mock data is class-level by default

Blog::Mock.data["posts"] # ["x"]

reset! dimisses the data object.

client.data.object_id # 70199868585600
client.reset!
client.data["posts"]  # []
client.data.object_id # 70199868566840

clear removes existing keys and values but keeps the same object.

client.data["posts"] += ["y"] # ["y"]
client.data.object_id         # 70199868378300
client.clear
client.data["posts"]          # []
client.data.object_id         # 70199868378300
  • store and []= write
  • fetch and [] read

You can make the service bypass Cistern's mock data structures by simply creating a self.data function in your service Mock declaration.

class Blog
  include Cistern::Client

  class Mock
    def self.data
      @data ||= {}
    end
  end
end

Working with data

Cistern::Hash contains many useful functions for working with data normalization and transformation.

#stringify_keys

# anywhere
Cistern::Hash.stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
# within a Resource
hash_stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}

#slice

# anywhere
Cistern::Hash.slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
# within a Resource
hash_slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}

#except

# anywhere
Cistern::Hash.except({a: 1, b: 2}, :a) #=> {b: 2}
# within a Resource
hash_except({a: 1, b: 2}, :a) #=> {b: 2}

#except!

# same as #except but modify specified Hash in-place
Cistern::Hash.except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
# within a Resource
hash_except!({:a => 1, :b => 2}, :a) #=> {:b => 2}

Storage

Currently supported storage backends are:

  • :hash : Cistern::Data::Hash (default)
  • :redis : Cistern::Data::Redis

Backends can be switched by using store_in.

# use redis with defaults
Patient::Mock.store_in(:redis)
# use redis with a specific client
Patient::Mock.store_in(:redis, client: Redis::Namespace.new("cistern", redis: Redis.new(host: "10.1.0.1"))
# use a hash
Patient::Mock.store_in(:hash)

Dirty

Dirty attributes are tracked and cleared when merge_attributes is called.

  • changed returns a Hash of changed attributes mapped to there initial value and current value
  • dirty_attributes returns Hash of changed attributes with there current value. This should be used in the model save function.
post = Blog::Post.new(id: 1, flavor: "x") # => <#Blog::Post>

post.dirty?           # => false
post.changed          # => {}
post.dirty_attributes # => {}

post.flavor = "y"

post.dirty?           # => true
post.changed          # => {flavor: ["x", "y"]}
post.dirty_attributes # => {flavor: "y"}

post.save
post.dirty?           # => false
post.changed          # => {}
post.dirty_attributes # => {}

Custom Architecture

When configuring your client, you can use :collection, :request, and :model options to define the name of module or class interface for the service component.

For example: if you'd Request is to be used for a model, then the Request component name can be remapped to Demand

For example:

class Blog
  include Cistern::Client.with(interface: :modules, request: "Demand")
end

allows a model named Request to exist

class Blog::Request
  include Blog::Model

  identity :jovi
end

while living on a Demand

class Blog::GetPost
  include Blog::Demand

  def real
    cistern.request.get("/wing")
  end
end

~> 3.0

Request Dispatch

Default request interface passes through #_mock and #_real depending on the client mode.

class Blog::GetPost
  include Blog::Request

  def setup(post_id, parameters)
    [post_id, stringify_keys(parameters)]
  end

  def _mock(*args, **kwargs)
    mock(*setup(*args, **kwargs))
  end

  def _real(post_id, parameters)
    real(*setup(*args, **kwargs))
  end
end

In cistern 3, requests pass through #call in both modes. #dispatch is responsible for determining the mode and calling the appropriate method.

class Blog::GetPost
  include Blog::Request

  def call(post_id, parameters)
    normalized_parameters = stringify_keys(parameters)
    dispatch(post_id, normalized_parameters)
  end
end

Client definition

Default resource definition is done by inheritance.

class Blog::Post < Blog::Model
end

In cistern 3, resource definition is done by module inclusion.

class Blog::Post
  include Blog::Post
end

Prepare for cistern 3 by using Cistern::Client.with(interface: :module) when defining the client.

class Blog
  include Cistern::Client.with(interface: :module)
end

Examples

Releasing

$ gem bump -trv (major|minor|patch)

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

cistern's People

Contributors

alenia avatar gitter-badger avatar gschorkopf avatar jaw6 avatar jhsu avatar jlindley avatar lanej avatar leklund avatar manuelmeurer avatar ryansouza avatar shaiguitar avatar thommahoney avatar valarissa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

cistern's Issues

Ruby 2.x Array#to_set removed

Description

Cistern's collection.rb uses to_set on BLACKLISTED_ARRAY_METHODS which is an Array.

However, in Ruby 2.x (http://www.ruby-doc.org/core-2.0/Array.html, http://www.ruby-doc.org/core-2.1.1/Array.html), the to_set method has been removed.

Expected Behaviour

Be able to use and run cistern in Ruby 2.x which is the default OSX version in Mavericks.

Actual Behaviour

Exception is thrown when using the collections:

/Library/Ruby/Gems/2.0.0/gems/cistern-0.5.3/lib/cistern/collection.rb:9:in `<class:Collection>': undefined method `to_set' for #<Array:0x007f8f4a3220b8> (NoMethodError)

How to reproduce

Instantiate a Collections object and invoke BLACKLISTED_ARRAY_METHODS

Possible Fix

Due to my restricted knowledge of Ruby and how to best include scripts, I could only test the fix with what I've been given :)

In collections.rb I've done the following:

require 'set'

class Cistern::Collection
  extend Cistern::Attributes::ClassMethods
  include Cistern::Attributes::InstanceMethods

  BLACKLISTED_ARRAY_METHODS = Set.new [
    :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
    :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
    :keep_if, :pop, :shift, :delete_at, :compact
  ] # :nodoc

This appears to fix the issue. Should I make a pull-request with this?

write a gem description

I don't know what this does.
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ
๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ ๐Ÿ’ฉ

rename `service` to `cistern`

  • service is a common term and can result in namespace conflicts
  • cistern is unique and descriptive

need to deprecate out service

Cistern::Model#inspect raises TypeError when @identity is not defined

The documentation says identity is "not required" but if you don't have it defined as an attribute you can't inspect a Model instance:

$ ruby -rcistern -e 'class MyService < Cistern::Service; end; MyService::Model.new.inspect'
/โ€ฆ/gems/cistern-2.0.2/lib/cistern/attributes.rb:176:in `identity': nil is not a symbol nor a string (TypeError)
    from /โ€ฆ/gems/cistern-2.0.2/lib/cistern/model.rb:30:in `inspect'
    from -e:1:in `<main>'

Can't squash on multiple fields

Consider the following

class Blah < Cistern::Model
  attribute :fanart, aliases: "Images", squash: "fanart"
  attribute :boxart, aliases: "Images", squash: "boxart"
end

When creating this model with a hash of:

{"Images" => {"fanart" => { stuff }, "boxart" => { stuff } }

The boxart attribute will end up with the contents of

{"fanart" => { stuff }, "boxart" => { stuff } }

The last attribute that you squash when you are trying to squash on the same alias more than once, will always end up with the complete squashed hash, earlier defined attributes end up nil.

Cistern::Model#new_record? raises TypeError when @identity is not set with 2.0.3

As in #47 the documentation says identity is "not required" but if you don't have it defined you get an unhelpful error when you call #new_record? on a Model instance:

$ ruby -e 'gem "cistern", "= 2.0.3"; require "cistern"; class MyService < Cistern::Service; end; p Cistern::VERSION; p MyService::Model.new.new_record?'
"2.0.3"
/โ€ฆ/gems/cistern-2.0.3/lib/cistern/attributes.rb:176:in `identity': nil is not a symbol nor a string (TypeError)
    from /โ€ฆ/gems/cistern-2.0.3/lib/cistern/attributes.rb:225:in `new_record?'
    from -e:1:in `<main>'

This behavior is new in 2.0.3 and did not exist in 2.0.2:

$ ruby -e 'gem "cistern", "= 2.0.2"; require "cistern"; class MyService < Cistern::Service; end; p Cistern::VERSION; p MyService::Model.new.new_record?'
"2.0.2"

PS
The service we're wrapping has a compound identifier so Cistern::Attributes::identity is insufficient. I can work around this bug in the mean time, though.

Create models/collection from request

I really like the cistern approach but a quickstart guide that explains the basic functionalities would be nice.
I'm happy to contribute this once my first API wrapper is done. :)

How do I create/return models or a collection for a request?
I created a request that fetches a list of items (e.g. like this: https://github.com/lanej/zendesk2/blob/master/lib/zendesk2/client/requests/get_tickets.rb) but it returns a Faraday::Response right now (I use Faraday like the zendesk2 gem). How do I make it return a collection object?

On top of that, the response looks like this:

{
  query: "stuff",
  items: 10,
  accesskey: "123",
  operation: "itemsearch",
  page: 1
},
items: {
  totalresults: 356,
  totalpages: 36,
  item: [
    {
      item data
    },
    ...

How can I extract the totalresults and totalpages fields as well?

Thanks for a great gem! :)

collection method `load_records` should pass parameters

When loading a collection if you do

cistern.things(page: 1, per_page: 3).each {|thing| thing.call }

the parameters won't get passed through to the all call that the load_records method calls.

It works if you call

cistern.things.all(page: 1, per_page: 3).each {|thing| thing.call }

New version of the gem?

Hey there, sorry to be a bother... With the merging of #86 and #87, it seems like it might be worthwhile to cut a new version of the gem to allow updating to Ruby 3 and handling the use case highlighted in #87.

If needed/desired, I could do this if I'm added to https://rubygems.org/gems/cistern My username is the same there as it is here, valarissa, if that's the route you wanted to take.

Totally understand if that's not a desired course of action.

Optional coverage feature creates too many NoMethodErrors

While debugging an application using cistern I needed to run with -d to determine the cause of a segmentation fault (it was a SystemStackError that overflowed just a bit too much).

However, when looking at the output, I see many repetitions of:

Exception `NoMethodError' at /โ€ฆ/gems/cistern-2.2.3/lib/cistern/attributes.rb:126 - undefined method `+' for nil:NilClass

This is line 126 of attributes.rb and it should use feature detection like the accompanying work over here. Without this a stack trace is generated and destroyed every time an attribute is read (which can be a large object).

Two attributes with the same alias

I have the following response format:

{
  price: {
    amount: 179,
    currency: "EUR"
  }
}

I would like to have the attributes amount and currency, so I tried this

attribute :price, aliases: 'price', squash: 'amount'
attribute :currency, aliases: 'price', squash: 'currency'

Problem is that the alias of the second attribute overwrites the first attribute and the resulting object only has currency but no price.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.