hyperclient 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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