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)