hyperclient 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
7
6
  InstalledFiles
8
7
  _yardoc
9
8
  coverage
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm use --create 1.9.3-p125@hyperclient
1
+ rvm use --create 1.9.3@hyperclient
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gem 'rake'
6
6
  gem 'growl'
7
7
  gem 'guard'
8
8
  gem 'guard-minitest'
9
+ gem 'guard-spinach'
9
10
  gem 'pry'
10
11
 
11
12
  gem 'redcarpet'
data/Gemfile.lock ADDED
@@ -0,0 +1,96 @@
1
+ GIT
2
+ remote: git://github.com/rubyworks/yard-tomdoc
3
+ revision: caee83fb8b068fef81068e00dc8d1245354536f1
4
+ specs:
5
+ yard-tomdoc (0.5.0)
6
+ tomparse
7
+ yard
8
+
9
+ PATH
10
+ remote: .
11
+ specs:
12
+ hyperclient (0.3.0)
13
+ faraday (~> 0.8)
14
+ faraday_middleware (~> 0.9)
15
+ net-http-digest_auth (~> 1.2)
16
+ uri_template (~> 0.5)
17
+
18
+ GEM
19
+ remote: https://rubygems.org/
20
+ specs:
21
+ addressable (2.2.8)
22
+ ansi (1.4.2)
23
+ coderay (1.0.6)
24
+ colorize (0.5.8)
25
+ crack (0.3.1)
26
+ faraday (0.8.4)
27
+ multipart-post (~> 1.1)
28
+ faraday_middleware (0.9.0)
29
+ faraday (>= 0.7.4, < 0.9)
30
+ ffi (1.0.11)
31
+ gherkin-ruby (0.2.1)
32
+ growl (1.0.3)
33
+ guard (1.0.3)
34
+ ffi (>= 0.5.0)
35
+ thor (>= 0.14.6)
36
+ guard-minitest (0.5.0)
37
+ guard (>= 0.4)
38
+ guard-spinach (0.0.1.1)
39
+ guard
40
+ spinach
41
+ metaclass (0.0.1)
42
+ method_source (0.7.1)
43
+ minitest (3.4.0)
44
+ mocha (0.13.1)
45
+ metaclass (~> 0.0.1)
46
+ multi_json (1.3.6)
47
+ multipart-post (1.1.5)
48
+ net-http-digest_auth (1.2.1)
49
+ pry (0.9.9.6)
50
+ coderay (~> 1.0.5)
51
+ method_source (~> 0.7.1)
52
+ slop (>= 2.4.4, < 3)
53
+ rack (1.4.1)
54
+ rack-test (0.6.2)
55
+ rack (>= 1.0)
56
+ rake (0.9.2.2)
57
+ redcarpet (2.1.1)
58
+ simplecov (0.6.4)
59
+ multi_json (~> 1.0)
60
+ simplecov-html (~> 0.5.3)
61
+ simplecov-html (0.5.3)
62
+ slop (2.4.4)
63
+ spinach (0.7.0)
64
+ colorize
65
+ gherkin-ruby (~> 0.2.0)
66
+ thor (0.15.2)
67
+ tomparse (0.2.1)
68
+ turn (0.9.5)
69
+ ansi
70
+ uri_template (0.5.1)
71
+ webmock (1.8.7)
72
+ addressable (>= 2.2.7)
73
+ crack (>= 0.1.7)
74
+ yard (0.8.2.1)
75
+
76
+ PLATFORMS
77
+ ruby
78
+
79
+ DEPENDENCIES
80
+ growl
81
+ guard
82
+ guard-minitest
83
+ guard-spinach
84
+ hyperclient!
85
+ minitest (~> 3.4.0)
86
+ mocha (~> 0.13)
87
+ pry
88
+ rack-test (~> 0.6)
89
+ rake
90
+ redcarpet
91
+ simplecov
92
+ spinach
93
+ turn (~> 0.9)
94
+ webmock (~> 1.8)
95
+ yard (~> 0.8)
96
+ yard-tomdoc!
data/Guardfile CHANGED
@@ -4,3 +4,10 @@ guard 'minitest' do
4
4
  watch(%r|^(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
5
5
  watch(%r|^test/test_helper\.rb|) { "test" }
6
6
  end
7
+
8
+ guard 'spinach' do
9
+ watch(%r|^features/(.*)\.feature|)
10
+ watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m|
11
+ "features/#{m[1]}#{m[2]}.feature"
12
+ end
13
+ end
data/Rakefile CHANGED
@@ -24,5 +24,9 @@ Rake::TestTask.new(:test) do |t|
24
24
  t.verbose = false
25
25
  end
26
26
 
27
+ desc 'runs the whole spinach suite'
28
+ task :spinach do
29
+ ruby '-S spinach'
30
+ end
27
31
 
28
- task :default => :test
32
+ task default: [:test, :spinach]
data/Readme.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Hyperclient
2
2
  [![Build Status](https://secure.travis-ci.org/codegram/hyperclient.png)](http://travis-ci.org/codegram/hyperclient)
3
3
  [![Dependency Status](https://gemnasium.com/codegram/hyperclient.png)](http://gemnasium.com/codegram/hyperclient)
4
- [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/codegram/hyperclient)
4
+ [![Code Climate](https://codeclimate.com/github/codegram/hyperclient.png)](https://codeclimate.com/github/codegram/hyperclient)
5
5
 
6
6
  Hyperclient is a Ruby Hypermedia API client written in Ruby.
7
7
 
@@ -14,14 +14,15 @@ Hyperclient is a Ruby Hypermedia API client written in Ruby.
14
14
  Example API client:
15
15
 
16
16
  ````ruby
17
- options = {}
18
- options[:auth] = {type: :digest, user:, 'user', password: 'password'}
19
- options[:headers] = {'accept-encoding' => 'deflate, gzip'}
20
- options[:debug] = true
21
-
22
- api = Hyperclient::EntryPoint.new('http://myapp.com/api', options)
17
+ api = Hyperclient.new('http://myapp.com/api').tap do |api|
18
+ api.digest_auth('user', 'password')
19
+ api.headers.merge({'accept-encoding' => 'deflate, gzip'})
20
+ end
23
21
  ````
24
22
 
23
+ By default, Hyperclient adds `application/json` as `Content-Type` and `Accept`
24
+ headers. It will also sent requests as JSON and parse JSON responses.
25
+
25
26
  [More examples][examples]
26
27
 
27
28
  ## HAL
@@ -96,7 +97,7 @@ api.embedded.posts.first.attributes
96
97
  OK, navigating an API is really cool, but you may want to actually do something
97
98
  with it, right?
98
99
 
99
- Hyperclient uses [HTTParty][httparty] under the hood to perform HTTP calls. You can
100
+ Hyperclient uses [Faraday][faraday] under the hood to perform HTTP calls. You can
100
101
  call any valid HTTP method on any Resource:
101
102
 
102
103
  ````ruby
@@ -104,6 +105,7 @@ post = api.embedded.posts.first
104
105
  post.get
105
106
  post.head
106
107
  post.put({title: 'New title'})
108
+ post.patch({title: 'New title'})
107
109
  post.delete
108
110
  post.options
109
111
 
@@ -118,6 +120,14 @@ api.links.post.expand(:id => 3).first
118
120
  # => #<Resource ...>
119
121
  ````
120
122
 
123
+ You can access the Faraday connection (to add middlewares or do whatever
124
+ you want) by calling `connection` on the entry point. As an example, you could use the [faraday-http-cache-middleware](https://github.com/plataformatec/faraday-http-cache)
125
+ :
126
+
127
+ ````ruby
128
+ api.connection.use :http_cache
129
+ ````
130
+
121
131
  ## Other
122
132
 
123
133
  There's also a PHP library named [HyperClient](https://github.com/FoxyCart/HyperClient), if that's what you were looking for :)
@@ -151,7 +161,7 @@ MIT License. Copyright 2012 [Codegram Technologies][codegram]
151
161
  [contributors]: https://github.com/codegram/hyperclient/contributors
152
162
  [codegram]: http://codegram.com
153
163
  [documentup]: http://codegram.github.com/hyperclient
154
- [httparty]: http://github.com/jnunemaker/httparty
164
+ [faraday]: http://github.com/lostisland/faraday
155
165
  [examples]: http://github.com/codegram/hyperclient/tree/master/examples
156
166
  [enumerable]: http://ruby-doc.org/core-1.9.3/Enumerable.html
157
167
  [rdoc]: http://rubydoc.org/github/codegram/hyperclient/master/frames
@@ -0,0 +1,23 @@
1
+ Feature: API navigation
2
+ In order to get the data from my API
3
+ As a user
4
+ I want to navigate through the API
5
+
6
+ Scenario: Links
7
+ When I connect to the API
8
+ Then I should be able to navigate to posts and authors
9
+
10
+ Scenario: Templated links
11
+ Given I connect to the API
12
+ When I search for a post with a templated link
13
+ Then the API should receive the request with all the params
14
+
15
+ Scenario: Attributes
16
+ Given I connect to the API
17
+ When I load a single post
18
+ Then I should be able to access it's title and body
19
+
20
+ Scenario: Embedded resources
21
+ Given I connect to the API
22
+ When I load a single post
23
+ Then I should also be able to access it's embedded comments
@@ -0,0 +1,19 @@
1
+ Feature: Default config
2
+ In order to use HAL JSON apis
3
+ As a user
4
+ I want to make sure the default config is working
5
+
6
+ Scenario: JSON headers
7
+ Given I use the default hyperclient config
8
+ When I connect to the API
9
+ Then the request should have been sent with the correct JSON headers
10
+
11
+ Scenario: Send JSON data
12
+ Given I use the default hyperclient config
13
+ When I send some data to the API
14
+ Then it should have been encoded as JSON
15
+
16
+ Scenario: Parse JSON data
17
+ Given I use the default hyperclient config
18
+ When I get some data from the API
19
+ Then it should have been parsed as JSON
@@ -0,0 +1,33 @@
1
+ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps
2
+ include API
3
+
4
+ step 'I should be able to navigate to posts and authors' do
5
+ api.links.posts.resource
6
+ api.links['api:authors'].resource
7
+
8
+ assert_requested :get, 'http://api.example.org/posts'
9
+ assert_requested :get, 'http://api.example.org/authors'
10
+ end
11
+
12
+ step 'I search for a post with a templated link' do
13
+ api.links.search.expand(q: 'something').resource
14
+ end
15
+
16
+ step 'the API should receive the request with all the params' do
17
+ assert_requested :get, 'http://api.example.org/search?q=something'
18
+ end
19
+
20
+ step 'I load a single post' do
21
+ @post = api.links.posts.links.last_post
22
+ end
23
+
24
+ step 'I should be able to access it\'s title and body' do
25
+ @post.attributes.title.wont_equal nil
26
+ @post.attributes.body.wont_equal nil
27
+ end
28
+
29
+ step 'I should also be able to access it\'s embedded comments' do
30
+ comment = @post.embedded.comments.first
31
+ comment.attributes.title.wont_equal nil
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ class Spinach::Features::DefaultConfig < Spinach::FeatureSteps
2
+ include API
3
+
4
+ step 'I use the default hyperclient config' do
5
+ @api = Hyperclient.new('http://api.example.org')
6
+ end
7
+
8
+ step 'the request should have been sent with the correct JSON headers' do
9
+ assert_requested :get, 'api.example.org', headers: {'Content-Type' => 'application/json', 'Accept' => 'application/json'}
10
+ end
11
+
12
+ step 'I send some data to the API' do
13
+ stub_request(:post, "http://api.example.org/posts")
14
+ api.links.posts.post({title: 'My first blog post'})
15
+ end
16
+
17
+ step 'it should have been encoded as JSON' do
18
+ assert_requested :post, 'api.example.org/posts', body: '{"title":"My first blog post"}'
19
+ end
20
+
21
+ step 'I get some data from the API' do
22
+ @posts = api.links.posts
23
+ end
24
+
25
+ step 'it should have been parsed as JSON' do
26
+ @posts.attributes.total_posts.to_i.must_equal 9
27
+ @posts.attributes['total_posts'].to_i.must_equal 9
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'fixtures'
2
+ module API
3
+ include Spinach::DSL
4
+ include WebMock::API
5
+ include Spinach::Fixtures
6
+
7
+ before do
8
+ stub_request(:any, %r{api.example.org*}).to_return(body: root_response, headers:{'Content-Type' => 'application/json'})
9
+ stub_request(:get, 'api.example.org/posts').to_return(body: posts_response, headers: {'Content-Type' => 'application/json'})
10
+ stub_request(:get, 'api.example.org/posts/1').to_return(body: post_response, headers: {'Content-Type' => 'application/json'})
11
+ end
12
+
13
+ def api
14
+ @api ||= Hyperclient.new('http://api.example.org')
15
+ end
16
+
17
+ step 'I connect to the API' do
18
+ api.links
19
+ end
20
+
21
+ after do
22
+ WebMock.reset!
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ require 'minitest/spec'
2
+ require 'webmock'
3
+ require 'hyperclient'
4
+ require 'pry'
@@ -0,0 +1,43 @@
1
+ require 'json'
2
+
3
+ module Spinach
4
+ module Fixtures
5
+ def root_response
6
+ '{
7
+ "_links": {
8
+ "self": { "href": "/" },
9
+ "posts": { "href": "/posts" },
10
+ "search": { "href": "/search{?q}", "templated": true },
11
+ "api:authors": { "href": "/authors" }
12
+ }
13
+ }'
14
+ end
15
+
16
+ def posts_response
17
+ '{
18
+ "_links": {
19
+ "self": { "href": "/posts" },
20
+ "last_post": {"href": "/posts/1"}
21
+ },
22
+ "total_posts": "9"
23
+ }'
24
+ end
25
+
26
+ def post_response
27
+ '{
28
+ "_links": {
29
+ "self": { "href": "/posts/1" }
30
+ },
31
+ "title": "My first blog post",
32
+ "body": "Lorem ipsum dolor sit amet",
33
+ "_embedded": {
34
+ "comments": [
35
+ {
36
+ "title": "Some comment"
37
+ }
38
+ ]
39
+ }
40
+ }'
41
+ end
42
+ end
43
+ end
data/hyperclient.gemspec CHANGED
@@ -14,13 +14,15 @@ Gem::Specification.new do |gem|
14
14
  gem.require_paths = ["lib"]
15
15
  gem.version = Hyperclient::VERSION
16
16
 
17
- gem.add_dependency 'faraday'
18
- gem.add_dependency 'uri_template'
19
- gem.add_dependency 'net-http-digest_auth'
17
+ gem.add_dependency 'faraday', '~> 0.8'
18
+ gem.add_dependency 'faraday_middleware', '~> 0.9'
19
+ gem.add_dependency 'uri_template', '~> 0.5'
20
+ gem.add_dependency 'net-http-digest_auth', '~> 1.2'
20
21
 
21
22
  gem.add_development_dependency 'minitest', '~> 3.4.0'
22
- gem.add_development_dependency 'turn'
23
- gem.add_development_dependency 'webmock'
24
- gem.add_development_dependency 'mocha'
25
- gem.add_development_dependency 'rack-test'
23
+ gem.add_development_dependency 'turn', '~> 0.9'
24
+ gem.add_development_dependency 'webmock', '~> 1.8'
25
+ gem.add_development_dependency 'mocha', '~> 0.13'
26
+ gem.add_development_dependency 'rack-test', '~> 0.6'
27
+ gem.add_development_dependency 'spinach'
26
28
  end
@@ -0,0 +1,17 @@
1
+ require 'faraday'
2
+ require_relative 'request/digest_authentication'
3
+
4
+ module Faraday
5
+ # Reopen Faraday::Connection to add a helper to set the digest auth data.
6
+ class Connection
7
+ # Public: Adds the digest auth middleware at the top and sets the user and
8
+ # password.
9
+ #
10
+ # user - A String with the user.
11
+ # password - A String with the password.
12
+ #
13
+ def digest_auth(user, password)
14
+ self.builder.insert(0, Faraday::Request::DigestAuth, user, password)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,83 @@
1
+ require 'faraday'
2
+ require 'net/http/digest_auth'
3
+
4
+ module Faraday
5
+ # Public: A Faraday middleware to use digest authentication. Since order of
6
+ # middlewares do care, it should be the first one of the Request middlewares
7
+ # in order to work properly (due to how digest authentication works).
8
+ #
9
+ # If some requests using the connection don't need to use digest auth you
10
+ # don't have to worry, the middleware will do nothing.
11
+ #
12
+ # It uses Net::HTTP::DigestAuth to generate the authorization header but it
13
+ # should work with any adapter.
14
+ #
15
+ # Examples:
16
+ #
17
+ # connection = Faraday.new(...) do |connection|
18
+ # connection.request :digest, USER, PASSWORD
19
+ # end
20
+ #
21
+ # # You can also use it later with a connection:
22
+ # connection.digest_auth('USER', 'PASSWORD')
23
+ #
24
+ class Request::DigestAuth < Faraday::Middleware
25
+
26
+ # Public: Initializes a DigestAuth.
27
+ #
28
+ # app - The Faraday app.
29
+ # user - A String with the user to authentication the connection.
30
+ # password - A String with the password to authentication the connection.
31
+ def initialize(app, user, password)
32
+ super(app)
33
+ @user, @password = user, password
34
+ end
35
+
36
+ # Public: Sends a first request with an empty body to get the
37
+ # authentication headers and then send the same request with the body and
38
+ # authorization header.
39
+ #
40
+ # env - A Hash with the request environment.
41
+ #
42
+ # Returns a Faraday::Response.
43
+ def call(env)
44
+ response = handshake(env)
45
+ return response unless response.status == 401
46
+
47
+ env[:request_headers]['Authorization'] = header(response)
48
+ @app.call(env)
49
+ end
50
+
51
+ private
52
+ # Internal: Sends the the request with an empry body.
53
+ #
54
+ # env - A Hash with the request environment.
55
+ #
56
+ # Returns a Faraday::Response.
57
+ def handshake(env)
58
+ env_without_body = env.dup
59
+ env_without_body.delete(:body)
60
+
61
+ @app.call(env_without_body)
62
+ end
63
+
64
+ # Internal: Builds the authorization header with the authentication data.
65
+ #
66
+ # response - A Faraday::Response with the authenticate headers.
67
+ #
68
+ # Returns a String with the DigestAuth header.
69
+ def header(response)
70
+ uri = response.env[:url]
71
+ uri.user = @user
72
+ uri.password = @password
73
+
74
+ realm = response.headers['www-authenticate']
75
+ method = response.env[:method].to_s.upcase
76
+
77
+ Net::HTTP::DigestAuth.new.auth_header(uri, realm, method)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Register the middleware as a Request middleware with the name :digest
83
+ Faraday.register_middleware :request, digest: Faraday::Request::DigestAuth
@@ -14,7 +14,11 @@ module Hyperclient
14
14
  # representation - The hash with the HAL representation of the Resource.
15
15
  #
16
16
  def initialize(representation)
17
- @collection = representation.delete_if {|key, value| key =~ /^_/}
17
+ @collection = if representation.is_a?(Hash)
18
+ representation.delete_if {|key, value| key =~ /^_/}
19
+ else
20
+ representation
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -41,6 +41,10 @@ module Hyperclient
41
41
  @collection.to_hash
42
42
  end
43
43
 
44
+ def to_s
45
+ to_hash
46
+ end
47
+
44
48
  # Public: Provides method access to the collection values.
45
49
  #
46
50
  # It allows accessing a value as `collection.name` instead of
@@ -48,7 +52,9 @@ module Hyperclient
48
52
  #
49
53
  # Returns an Object.
50
54
  def method_missing(method_name, *args, &block)
51
- @collection.fetch(method_name.to_s) { super }
55
+ @collection.fetch(method_name.to_s) do
56
+ raise "Could not find `#{method_name.to_s}` in #{self.class.name}"
57
+ end
52
58
  end
53
59
 
54
60
  # Internal: Accessory method to allow the collection respond to the
@@ -1,4 +1,6 @@
1
1
  require 'hyperclient/link'
2
+ require 'faraday_middleware'
3
+ require_relative '../faraday/connection'
2
4
 
3
5
  module Hyperclient
4
6
  # Public: The EntryPoint is the main public API for Hyperclient. It is used to
@@ -6,44 +8,52 @@ module Hyperclient
6
8
  #
7
9
  # Examples
8
10
  #
9
- # options = {}
10
- # options[:headers] = {'accept-encoding' => 'deflate, gzip'}
11
- # options[:auth] = {type: 'digest', user: 'foo', password: 'secret'}
12
- # options[:debug] = true
11
+ # client = Hyperclient::EntryPoint.new('http://my.api.org')
13
12
  #
14
- # client = Hyperclient::EntryPoint.new('http://my.api.org', options)
15
- #
16
- class EntryPoint
17
-
18
- # Public: Returns the Hash with the configuration.
19
- attr_accessor :config
13
+ class EntryPoint < Link
14
+ extend Forwardable
15
+ # Public: Delegates common methods to be used with the Faraday connection.
16
+ def_delegators :connection, :basic_auth, :digest_auth, :token_auth, :headers, :headers=, :params, :params=
20
17
 
21
18
  # Public: Initializes an EntryPoint.
22
19
  #
23
20
  # url - A String with the entry point of your API.
24
- # config - The Hash options used to setup the HTTP client (default: {})
25
- # See HTTP for more documentation.
26
- def initialize(url, config = {})
27
- @config = config.update(base_uri: url)
28
- @entry = Link.new({'href' => url}, self).resource
21
+ def initialize(url)
22
+ @link = {'href' => url}
23
+ @entry_point = self
29
24
  end
30
25
 
31
- # Internal: Delegate the method to the entry point Resource if it exists.
26
+ # Public: A Faraday connection to use as a HTTP client.
32
27
  #
33
- # This way we can call our API client with the resources name instead of
34
- # having to add the methods to it.
35
- def method_missing(method, *args, &block)
36
- if @entry.respond_to?(method)
37
- @entry.send(method, *args, &block)
38
- else
39
- super
28
+ # Returns a Faraday::Connection.
29
+ def connection
30
+ @connection ||= Faraday.new(url, {headers: default_headers}, &default_faraday_block)
31
+ end
32
+
33
+ private
34
+ # Internal: Returns a block to initialize the Faraday connection. The
35
+ # default block includes a middleware to encode requests as JSON, a
36
+ # response middleware to parse JSON responses and sets the adapter as
37
+ # NetHttp.
38
+ #
39
+ # These middleware can always be changed by accessing the Faraday
40
+ # connection.
41
+ #
42
+ # Returns a block.
43
+ def default_faraday_block
44
+ lambda do |faraday|
45
+ faraday.request :json
46
+ faraday.response :json, content_type: /\bjson$/
47
+ faraday.adapter :net_http
40
48
  end
41
49
  end
42
50
 
43
- # Internal: Accessory method to allow the entry point respond to the
44
- # methods that will hit method_missing.
45
- def respond_to_missing?(method, include_private = false)
46
- @entry.respond_to?(method.to_s)
51
+ # Internal: Returns the default headers to initialize the Faraday connection.
52
+ # The default headers et the Content-Type and Accept to application/json.
53
+ #
54
+ # Returns a Hash.
55
+ def default_headers
56
+ {'Content-Type' => 'application/json', 'Accept' => 'application/json'}
47
57
  end
48
58
  end
49
59
  end