chain 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 96a912a49313f8b3e5f4f384c4b85e421c7950fb
4
+ data.tar.gz: 95bf5be6688445559aa4d5afa0144e9d7ce1f2a8
5
+ SHA512:
6
+ metadata.gz: 0bf5415f4bd29a8d98a1a31e2077f76d3ff3959dfeeafdd826ef1df002d7e7e9d99cadeeaa2bc095038bc80d0e2b27b1735b5ceb732b4fb83b9f0b88b3df74c6
7
+ data.tar.gz: f6f9e422551146b173d85f60383b8e1bea9a298b7ad84c7ddbf564f478a31987c60337ed7d98a1414aebacd1beec362577938a3059b5f5f593bee07b84b2f01c
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ vendor/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chain.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Stefan Novak
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ # Chain #
2
+
3
+ *Abusing `method_missing` since 2013*™
4
+
5
+ ### What in the Sam Hill is Chain? ###
6
+
7
+ Chain is a simple library that makes it (too) easy to interface with a (non)-RESTful web API. Inspired by [Her](https://github.com/remiprev/her/), I needed a way to create something that mimics an ORM to communicate with a non-RESTful API. Chain uses [Faraday](https://github.com/lostisland/faraday) as the client library to manage requests to API endpoints. As a result, you have full control of how the request and response are parsed out and mapped to an object in Ruby!
8
+
9
+ ### How does it work? ###
10
+
11
+ Simply instantiate the `Url` class and then chain together a series of methods that represent the URL path. Finish off the chain with a bang to kick off the request. For example:
12
+
13
+ ```ruby
14
+ >> require 'chain'
15
+
16
+ >> site = Chain::Url("http://www.site.com")
17
+
18
+ >> item = site.items[1].group!
19
+ => #Hashie::Mash of JSON from http://www.site.com/items/1/group
20
+
21
+ >> item.name
22
+ => "..."
23
+ ```
24
+
25
+ This opens up all sorts of clever ways to iterate through an endpoint:
26
+
27
+ ```ruby
28
+ # Send a GET request to http://www.site.com/items
29
+ items = site.items!
30
+
31
+ # Assuming that /items returns a JSON object containing a list of items in the `data` attribute...
32
+ items.data.each do |item|
33
+
34
+ # ...iterate through and print out the `name` attribute for http://www.site.com/items/#
35
+ puts items[item.id]!.name
36
+ end
37
+ ```
38
+
39
+ ### Query Parameters ###
40
+
41
+ You can specify query parameters via the `[]`, `_fetch`, or `_<insert your favorite http verb here>`. For example:
42
+
43
+ ```ruby
44
+ # Submit a GET request to http://www.site.com/users?name=Mark Corrigan
45
+
46
+ >> user = site.users[name: 'Mark Corrigan']
47
+ >> user = site[:users, name: 'Mark Corrigan']
48
+ >> user = site.users._fetch(name: 'Mark Corrigan`)
49
+ ```
50
+
51
+ ### Other HTTP actions ###
52
+
53
+ By default, the `!`, `[]`, and `_fetch` methods on a `Url` object will map to a GET request. You can also use `_put`, `_post`, `_head`, `_delete`, etc.
54
+
55
+ To submit a request with a payload, send a request with the `_body` parameter:
56
+
57
+ ```ruby
58
+ # Send a POST request to http://www.site.com/users with URL-encoded parameters in the payload
59
+ >> user = site.users._post(_body: {name: 'Mark Corrigan'})
60
+ ```
61
+
62
+ You can also manually specify the HTTP verb via `_method` and headers via `_headers`:
63
+
64
+ ```ruby
65
+ >> user = site.users._fetch(_method: :post, _body: {name: 'Mark Corrigan'})
66
+ >> site.users._fetch(_method: :post, _headers: {"Accept" => "text/plain"})
67
+ ```
68
+
69
+ ### Configuring the Middleware ###
70
+
71
+ By default, Chain will assume that the response is JSON and will render that object inside of a [Hashie::Mash](https://github.com/intridea/hashie) object. If you want to implement your own request/response middleware, simply pass in a block to configure the Faraday connection:
72
+
73
+ ```ruby
74
+ site = Chain.Url("http://www.site.com") do |connection|
75
+ connection.use Faraday::Request::UrlEncoded
76
+ connection.use MyResponseMiddleWare
77
+ connection.use Faraday::Adapter::NetHttp
78
+ end
79
+ ```
80
+
81
+ Writing your own middleware is fairly easy. Chain uses something along the lines of:
82
+
83
+ ```ruby
84
+ class HashieMashResponse < Faraday::Response::Middleware
85
+ def on_complete(env)
86
+ body = JSON.parse(env[:body])
87
+ headers = env[:response_headers]
88
+ env[:body] = Hashie::Mash.new(body).tap do |item|
89
+ item._headers = Hashie::Mash.new(headers)
90
+ item._status = env[:status]
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Bring on the caveats! ###
97
+
98
+ 1. You cannot follow the bracket notation by a bang, such as: `site.person["Mark Corrgian"]!`. Use `site.person["Mark Corrgian"]._fetch`.
99
+
100
+ 2. You cannot pass in a block to the bracket method, such as: `site.person["Mark Corrgian"]{|req| p req['url']}`. Use the `_fetch` method as described above.
101
+
102
+ 3. For any urls that end with an extension, you will need to use the bracket notation. For example, site.users.json would render http://www.site.com/users/json.
103
+
104
+ 4. Any portions of the url path that contain characters not supported by Ruby, you will need to use the bracket notation. This includes path segments that start with numerics, such as http://www.site.com/users/0. (It cannot be rendered as `site.users.0`, but rather `site.users[0]`.)
105
+
106
+ 5. You will not be able to access URL sub-paths that have names similar to methods on standard objects in Ruby. For example, site.users[1].methods (http://www.site.com/users/1/methods) will return you a list of methods on the Chain::Url object. Use bracket notation!
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chain/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "chain"
8
+ spec.version = Chain::VERSION
9
+ spec.authors = ["Stefan Novak"]
10
+ spec.email = ["stefan.louis.novak@gmail.com"]
11
+ spec.description = %q{Access API endpoints via method chaining in Ruby.}
12
+ spec.summary = %q{Access API endpoints via method chaining in Ruby.}
13
+ spec.homepage = "https://github.com/slnovak/chain"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", "~> 2.14.1"
24
+ spec.add_development_dependency "webmock", "~> 1.15.2"
25
+
26
+ spec.add_dependency "faraday", "~> 0.8.8"
27
+ spec.add_dependency "hashie", "~> 2.0.5"
28
+ end
@@ -0,0 +1,9 @@
1
+ require "uri"
2
+ require "json"
3
+
4
+ require "faraday"
5
+ require "hashie"
6
+
7
+ require "chain/url"
8
+ require "chain/version"
9
+ require "chain/middleware/hashie_mash_response"
@@ -0,0 +1,14 @@
1
+ module Chain
2
+ module Middleware
3
+ class HashieMashResponse < Faraday::Response::Middleware
4
+ def on_complete(env)
5
+ body = JSON.parse(env[:body].to_s.encode('UTF-8', {:invalid => :replace, :undef => :replace, :replace => '?'}))
6
+ headers = env[:response_headers]
7
+ env[:body] = Hashie::Mash.new(body).tap do |item|
8
+ item._headers = Hashie::Mash.new(headers)
9
+ item._status = env[:status]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,87 @@
1
+ module Chain
2
+ class Url
3
+ attr_accessor :connection, :default_parameters
4
+
5
+ def initialize(url, base_url=nil, params={}, &block)
6
+ @url = url
7
+
8
+ # Shift method arguments
9
+ if base_url.is_a? Hash
10
+ base_url, params = nil, base_url
11
+ end
12
+
13
+ @base_url = base_url
14
+
15
+ @default_parameters = params.delete(:_default_parameters)
16
+
17
+ if base_url.nil?
18
+ # If no base_url is given, then this is a 'base' instance of a Url. If a block is given,
19
+ # yield our connection object to the block so that the base instance can have its own
20
+ # Faraday configuration
21
+ @connection = Faraday.new(url: @url) do |connection|
22
+ if block_given?
23
+ yield connection
24
+ else
25
+ connection.use Faraday::Request::UrlEncoded
26
+ connection.use Middleware::HashieMashResponse
27
+ connection.use Faraday::Adapter::NetHttp
28
+ end
29
+ end
30
+ else
31
+ # If this is a new instance with a base_url (from method_missing, let's say), then we're
32
+ # going to yield a request object to the block so a user can configure it.
33
+ if block_given?
34
+ _fetch(params, &block)
35
+ end
36
+ end
37
+ end
38
+
39
+ def method_missing(method_name, path=nil, params={}, &block)
40
+
41
+ # If this is a bang method, prepare to run a _fetch.
42
+ is_bang_method = method_name.to_s.chars.last == "!"
43
+ method_name = method_name[0...-1] if is_bang_method
44
+
45
+ # If a subsequent path is given as an argument, then expand out the path appropriately.
46
+ # For example, we might have base.api.items("My item", f: 'json'){|request| ...}. This
47
+ # should call into #{base}/api/items/My%20Item?f=json
48
+ combined_path = [method_name, path].compact.join("/")
49
+
50
+ url = URI.join("#{@url}/", combined_path)
51
+
52
+ self.class.new(url, @base_url || self, params, &block).tap do |request|
53
+ return request._fetch(params, &block) if is_bang_method || !params.empty?
54
+ end
55
+ end
56
+
57
+ def [](path=nil, params={})
58
+ if path.is_a? Hash
59
+ path, params = nil, path.merge(params)
60
+ end
61
+
62
+ url = [@url, path].compact.join("/")
63
+ method_missing(url, nil, **params)
64
+ end
65
+
66
+ def _fetch(params={}, &block)
67
+ http_method = params.delete(:_method) || :get
68
+ body = params.delete(:_body)
69
+ headers = params.delete(:_headers)
70
+
71
+ @base_url.connection.run_request(http_method, @url, body, headers){|request|
72
+ request.params.update(params) if params
73
+ request.params.update(@base_url.default_parameters) if @base_url.default_parameters
74
+ yield request if block_given?
75
+ }.body
76
+ end
77
+
78
+ %w[get head delete post put patch].each do |verb|
79
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
80
+ def _#{verb}(params={}, &block)
81
+ params.merge!(_method: :#{verb})
82
+ _fetch(params, &block)
83
+ end
84
+ RUBY
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module Chain
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'json'
2
+ require 'webmock/rspec'
3
+
4
+ require 'chain'
@@ -0,0 +1,165 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chain::Url do
4
+
5
+ context 'with HashieMashMiddleware' do
6
+
7
+ subject do
8
+ described_class.new('http://test.com')
9
+ end
10
+
11
+ describe 'response payloads' do
12
+
13
+ before :each do
14
+ body = {data: true}.to_json
15
+
16
+ stub_request(:get, 'http://test.com/item').
17
+ to_return(status: 200, body: body)
18
+ end
19
+
20
+ after :each do
21
+ a_request(:get, 'http://test.com/item').
22
+ should have_been_made
23
+
24
+ expect(@item.data).to eq(true)
25
+ end
26
+
27
+ it 'should return a response payload using the bang method' do
28
+ @item = subject.item!
29
+ end
30
+
31
+ it 'should return a response payload using the _fetch method' do
32
+ @item = subject.item._fetch
33
+ end
34
+
35
+ it 'should parse a request url using the bracket method' do
36
+ @item = subject['item']._fetch
37
+ end
38
+ end
39
+
40
+ describe 'http verbs' do
41
+
42
+ [:get, :put, :post, :delete, :head, :patch].each do |verb|
43
+ it "should make valid request using the _#{verb} syntax" do
44
+ body = {data: true}.to_json
45
+
46
+ stub_request(verb, 'http://test.com/item').
47
+ to_return(status: 200, body: body)
48
+
49
+ subject.item.send("_#{verb}")
50
+
51
+ a_request(verb, 'http://test.com/item').
52
+ should have_been_made
53
+ end
54
+ end
55
+ end
56
+
57
+ describe 'request parsing' do
58
+
59
+ before :each do
60
+ body = {data: true}.to_json
61
+
62
+ stub_request(:get, 'http://test.com/item').
63
+ to_return(status: 200, body: body)
64
+
65
+ stub_request(:get, 'http://test.com/a/b/c/d/e/f/g').
66
+ to_return(status: 200, body: body)
67
+
68
+ stub_request(:get, 'http://test.com/item').
69
+ with(query: {foo: 'bar'}).
70
+ to_return(status: 200, body: body)
71
+
72
+ stub_request(:post, 'http://test.com/item').
73
+ to_return(status: 200, body: body)
74
+ end
75
+
76
+ it 'should parse chained methods as a nested url path' do
77
+ subject.a.b.c.d.e.f.g!
78
+
79
+ a_request(:get, 'http://test.com/a/b/c/d/e/f/g').
80
+ should have_been_made
81
+ end
82
+
83
+ it 'should take keyword arguments from bracket notion and expend them out as parameters' do
84
+ subject.item[foo: 'bar']._fetch
85
+
86
+ a_request(:get, 'http://test.com/item').
87
+ with(query: hash_including({'foo' => 'bar'})).
88
+ should have_been_made
89
+ end
90
+
91
+ it 'should parse the _method parameter as the HTTP request type' do
92
+ subject.item[_method: :post]._fetch
93
+
94
+ a_request(:post, 'http://test.com/item').
95
+ should have_been_made
96
+ end
97
+
98
+ it 'should parse the _body parameter as the HTTP request type' do
99
+ subject.item[_method: :post, _body: 'foo=bar']._fetch
100
+
101
+ a_request(:post, 'http://test.com/item').
102
+ with(body: 'foo=bar').
103
+ should have_been_made
104
+ end
105
+
106
+ it 'should parse the _headers parameter as the HTTP request headers' do
107
+ subject.item[:_headers => {'Content-Length' => '3'}]._fetch
108
+
109
+ a_request(:get, 'http://test.com/item').
110
+ with(headers: {'Content-Length' => '3'}).
111
+ should have_been_made
112
+ end
113
+ end
114
+
115
+ describe 'modifying Faraday requests' do
116
+
117
+ before :each do
118
+ body = {data: true}.to_json
119
+
120
+ stub_request(:get, 'http://test.com/item').
121
+ with(query: {foo: 'bar'}).
122
+ to_return(status: 200, body: body)
123
+ end
124
+
125
+ it 'should be able to modify the request object as a request is made' do
126
+ subject.item do |request|
127
+ request.params['foo'] = 'bar'
128
+ end
129
+
130
+ a_request(:get, 'http://test.com/item').
131
+ with(query: hash_including({'foo' => 'bar'})).
132
+ should have_been_made
133
+ end
134
+ end
135
+ end
136
+
137
+ context 'with HashieMashMiddleware and default parameters' do
138
+
139
+ subject do
140
+ described_class.new('http://test.com', _default_parameters: {foo: 'bar'})
141
+ end
142
+
143
+ describe 'request parsing' do
144
+
145
+ before :each do
146
+ body = {data: true}.to_json
147
+
148
+ stub_request(:get, 'http://test.com/item').
149
+ with(query: {foo: 'bar'}).
150
+ to_return(status: 200, body: body)
151
+
152
+ stub_request(:post, 'http://test.com/item').
153
+ to_return(status: 200, body: body)
154
+ end
155
+
156
+ it 'should parse out a url with default_parameters specified by the base url instance' do
157
+ subject.item!
158
+
159
+ a_request(:get, 'http://test.com/item').
160
+ with(query: hash_including({'foo' => 'bar'})).
161
+ should have_been_made
162
+ end
163
+ end
164
+ end
165
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chain
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Novak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 2.14.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.14.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.15.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 1.15.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: faraday
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.8
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.8
83
+ - !ruby/object:Gem::Dependency
84
+ name: hashie
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 2.0.5
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 2.0.5
97
+ description: Access API endpoints via method chaining in Ruby.
98
+ email:
99
+ - stefan.louis.novak@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .gitignore
105
+ - Gemfile
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - chain.gemspec
110
+ - lib/chain.rb
111
+ - lib/chain/middleware/hashie_mash_response.rb
112
+ - lib/chain/url.rb
113
+ - lib/chain/version.rb
114
+ - spec/spec_helper.rb
115
+ - spec/url_spec.rb
116
+ homepage: https://github.com/slnovak/chain
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.0.7
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: Access API endpoints via method chaining in Ruby.
140
+ test_files:
141
+ - spec/spec_helper.rb
142
+ - spec/url_spec.rb