README.md in cistern-2.6.0 vs README.md in cistern-2.7.0

- old
+ new

@@ -1,52 +1,24 @@ # Cistern +[![Join the chat at https://gitter.im/lanej/cistern](https://badges.gitter.im/lanej/cistern.svg)](https://gitter.im/lanej/cistern?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://secure.travis-ci.org/lanej/cistern.png)](http://travis-ci.org/lanej/cistern) [![Dependencies](https://gemnasium.com/lanej/cistern.png)](https://gemnasium.com/lanej/cistern.png) [![Gem Version](https://badge.fury.io/rb/cistern.svg)](http://badge.fury.io/rb/cistern) [![Code Climate](https://codeclimate.com/github/lanej/cistern/badges/gpa.svg)](https://codeclimate.com/github/lanej/cistern) Cistern helps you consistently build your API clients and faciliates building mock support. ## Usage -### Notice: Cistern 3.0 +### Client -Cistern 3.0 will change the way Cistern interacts with your `Request`, `Collection` and `Model` classes. +This represents the remote service that you are wrapping. It defines the client's namespace and initialization parameters. -Prior to 3.0, your `Request`, `Collection` and `Model` classes would have inherited from `<service>::Client::Request`, `<service>::Client::Collection` and `<service>::Client::Model` classes, respectively. +Client initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional. -In cistern `~> 3.0`, the default will be for `Request`, `Collection` and `Model` classes to instead include their respective `<service>::Client` modules. - -If you want to be forwards-compatible today, you can configure your client by using `Cistern::Client.with` - ```ruby -class Blog - include Cistern::Client.with(interface: :module) -end -``` - -Now request classes would look like: - -```ruby -class Blog::GetPost - include Blog::Request - - def real - "post" - end -end -``` - - -### Service - -This represents the remote service that you are wrapping. If the service name is `blog` then a good name is `Blog`. - -Service initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional. - -```ruby # lib/blog.rb class Blog include Cistern::Client requires :hmac_id, :hmac_secret @@ -60,12 +32,11 @@ # ArgumentError Blog.new(hmac_id: "1", url: "http://example.org") Blog.new(hmac_id: "1") ``` -Cistern will define for you two classes, `Mock` and `Real`. Create the corresponding files and initialzers for your -new service. +Cistern will define for two namespaced classes, `Blog::Mock` and `Blog::Real`. Create the corresponding files and initialzers for your new service. ```ruby # lib/blog/real.rb class Blog::Real attr_reader :url, :connection @@ -103,78 +74,73 @@ Blog.mocking? # false real.is_a?(Blog::Real) # true fake.is_a?(Blog::Mock) # true ``` -### Working with data +### Requests -`Cistern::Hash` contains many useful functions for working with data normalization and transformation. +Requests are defined by subclassing `#{service}::Request`. -**#stringify_keys** +* `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: + ```ruby -# 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} -``` +class Blog::UpdatePost + include Blog::Request -**#slice** + def real(id, parameters) + cistern.connection.patch("/post/#{id}", parameters) + end -```ruby -# 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} -``` + def mock(id, parameters) + post = cistern.data[:posts].fetch(id) -**#except** + post.merge!(stringify_keys(parameters)) -```ruby -# anywhere -Cistern::Hash.except({a: 1, b: 2}, :a) #=> {b: 2} -# within a Resource -hash_except({a: 1, b: 2}, :a) #=> {b: 2} + 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`. -**#except!** ```ruby -# 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} -``` +class Blog::UpdatePost + include Blog::Request + attr_reader :parameters -### Requests + def call(post_id, parameters) + @parameters = stringify_keys(parameters) + dispatch(Integer(post_id)) + end -Requests are defined by subclassing `#{service}::Request`. + def real(id) + cistern.connection.patch("/post/#{id}", parameters) + end -* `cistern` represents the associated `Blog` instance. + def mock(id) + post = cistern.data[:posts].fetch(id) -```ruby -class Blog::GetPost < Blog::Request - def real(params) - # make a real request - "i'm real" - end + post.merge!(parameters) - def mock(params) - # return a fake response - "imposter!" + response(post: post) end end - -Blog.new.get_post # "i'm real" ``` The `#cistern_method` function allows you to specify the name of the generated method. ```ruby -class Blog::GetPosts < Blog::Request +class Blog::GetPosts + include Blog::Request + cistern_method :get_all_the_posts def real(params) "all the posts" end @@ -262,11 +228,12 @@ For example: ```ruby -class Blog::Post < Blog::Model +class Blog::Post + include Blog::Model identity :id, type: :integer attribute :body attribute :author_id, aliases: "author", squash: "id" attribute :deleted_at, type: :time @@ -370,11 +337,12 @@ * `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 ```ruby -class Blog::Posts < Blog::Collection +class Blog::Posts + include Blog::Collection attribute :count, type: :integer model Blog::Post @@ -413,11 +381,13 @@ * `belongs_to` references a specific resource and defines a reader. * `has_many` references a collection of resources and defines a reader / writer. ```ruby -class Blog::Tag < Blog::Model +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) } @@ -433,11 +403,12 @@ tag.creator = blogs.author.get(name: 'phil') tag.attributes[:creator] #=> { 'id' => 2, 'name' => 'phil' } ``` -Foreign keys can be updated with association writing by overwriting the writer. +Foreign keys can be updated with with the association writer by aliasing the original writer and accessing the +underlying attributes. ```ruby Blog::Tag.class_eval do alias cistern_creator= creator= def creator=(creator) @@ -503,10 +474,52 @@ end end end ``` +### Working with data + +`Cistern::Hash` contains many useful functions for working with data normalization and transformation. + +**#stringify_keys** + +```ruby +# 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** + +```ruby +# 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** + +```ruby +# anywhere +Cistern::Hash.except({a: 1, b: 2}, :a) #=> {b: 2} +# within a Resource +hash_except({a: 1, b: 2}, :a) #=> {b: 2} +``` + + +**#except!** + +```ruby +# 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) @@ -586,13 +599,78 @@ cistern.request.get("/wing") end end ``` +## ~> 3.0 + +### Request Dispatch + +Default request interface passes through `#_mock` and `#_real` depending on the client mode. + +```ruby +class Blog::GetPost + include Blog::Request + + def setup(post_id, parameters) + [post_id, stringify_keys(parameters)] + end + + def _mock(*args) + mock(*setup(*args)) + end + + def _real(post_id, parameters) + real(*setup(*args)) + end +end +``` + +In cistern 3, requests pass through `#call` in both modes. `#dispatch` is responsible for determining the mode and +calling the appropriate method. + +```ruby +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. + +```ruby +class Blog::Post < Blog::Model +end +``` + +In cistern 3, resource definition is done by module inclusion. + +```ruby +class Blog::Post + include Blog::Post +end +``` + +Prepare for cistern 3 by using `Cistern::Client.with(interface: :module)` when defining the client. + +```ruby +class Blog + include Cistern::Client.with(interface: :module) +end +``` + ## Examples * [zendesk2](https://github.com/lanej/zendesk2) * [you_track](https://github.com/lanej/you_track) +* [ey-core](https://github.com/engineyard/core-client-rb) + ## Releasing $ gem bump -trv (major|minor|patch)