api_navigator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +26 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +4 -0
- data/Dangerfile +2 -0
- data/Gemfile +23 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +5 -0
- data/RELEASING.md +4 -0
- data/Rakefile +34 -0
- data/UPGRADING.md +4 -0
- data/api_navigator.gemspec +23 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/fixtures_2.rb +253 -0
- data/lib/api_navigator.rb +45 -0
- data/lib/api_navigator/attributes.rb +20 -0
- data/lib/api_navigator/collection_hash.rb +90 -0
- data/lib/api_navigator/curie.rb +47 -0
- data/lib/api_navigator/entry_point.rb +157 -0
- data/lib/api_navigator/link.rb +160 -0
- data/lib/api_navigator/link_collection.rb +63 -0
- data/lib/api_navigator/resource.rb +130 -0
- data/lib/api_navigator/resources/collection_resource.rb +41 -0
- data/lib/api_navigator/resources/member_resource.rb +63 -0
- data/lib/api_navigator/version.rb +3 -0
- data/lib/faraday/connection.rb +17 -0
- data/spec/fixtures/requests.rb +157 -0
- data/spec/fixtures/sample.json +108 -0
- data/spec/lib/api_navigator/attribute_spec.rb +36 -0
- data/spec/lib/api_navigator/collection_hash_spec.rb +71 -0
- data/spec/lib/api_navigator/entry_point_spec.rb +185 -0
- data/spec/lib/api_navigator/link_collection_spec.rb +77 -0
- data/spec/lib/api_navigator/link_spec.rb +343 -0
- data/spec/lib/api_navigator/resource_spec.rb +368 -0
- data/spec/spec_helper.rb +112 -0
- data/spec/support/book_resource.rb +10 -0
- data/spec/support/request_helper.rb +8 -0
- metadata +172 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'api_navigator/collection_hash'
|
2
|
+
require 'api_navigator/link'
|
3
|
+
require 'api_navigator/attributes'
|
4
|
+
require 'api_navigator/curie'
|
5
|
+
require 'api_navigator/entry_point'
|
6
|
+
require 'api_navigator/link_collection'
|
7
|
+
require 'api_navigator/resource'
|
8
|
+
require 'api_navigator/resources/member_resource'
|
9
|
+
require 'api_navigator/resources/collection_resource'
|
10
|
+
require 'api_navigator/version'
|
11
|
+
|
12
|
+
#
|
13
|
+
# @author [martinschweizer]
|
14
|
+
#
|
15
|
+
module ApiNavigator
|
16
|
+
|
17
|
+
ClientResourceClasses = {}
|
18
|
+
|
19
|
+
# @param url [String] The base url
|
20
|
+
# @param &block [type] Block for configuring the navitgator
|
21
|
+
#
|
22
|
+
# @return [ApiNavigator::EntryPoint] Then entrypoint hoocking it all up
|
23
|
+
def self.new(url, client_identifier=nil, &block)
|
24
|
+
ApiNavigator::EntryPoint.new(url, client_identifier, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def register(client_identifier)
|
29
|
+
raise "Already registered client_identifier: #{client_identifier}" if ClientResourceClasses.include?(client_identifier)
|
30
|
+
|
31
|
+
ClientResourceClasses[client_identifier] = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_resource(identifier, resource_class, client_identifier:)
|
35
|
+
ClientResourceClasses.fetch(client_identifier)[identifier] = resource_class
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource_class(identifier, client_identifier:)
|
39
|
+
result = ClientResourceClasses.fetch(client_identifier, {}).fetch(identifier, nil)
|
40
|
+
|
41
|
+
result.nil? ? ApiNavigator::Resources::MemberResource : result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApiNavigator
|
2
|
+
# Public: A wrapper class to easily acces the attributes in a Resource.
|
3
|
+
#
|
4
|
+
# Examples
|
5
|
+
#
|
6
|
+
# resource.attributes['title']
|
7
|
+
# resource.attributes.title
|
8
|
+
#
|
9
|
+
class Attributes < CollectionHash
|
10
|
+
# Public: Initializes the Attributes of a Resource.
|
11
|
+
#
|
12
|
+
# representation - The hash with the HAL representation of the Resource.
|
13
|
+
#
|
14
|
+
def initialize(attributes_hash)
|
15
|
+
raise ArgumentError, "argument nust be a Hash, is #{attributes_hash.class}" unless attributes_hash.kind_of? Hash
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module ApiNavigator
|
2
|
+
# Public: A helper class to wrap a collection of elements and provide
|
3
|
+
# Hash-like access or via a method call.
|
4
|
+
#
|
5
|
+
# Examples
|
6
|
+
#
|
7
|
+
# collection['value']
|
8
|
+
# collection.value
|
9
|
+
#
|
10
|
+
class CollectionHash
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
# Public: Initializes the Collection.
|
14
|
+
#
|
15
|
+
# collection - The Hash to be wrapped.
|
16
|
+
def initialize(collection)
|
17
|
+
@collection = collection
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Each implementation to allow the class to use the Enumerable
|
21
|
+
# benefits.
|
22
|
+
#
|
23
|
+
# Returns an Enumerator.
|
24
|
+
def each(&block)
|
25
|
+
@collection.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Checks if this collection includes a given key.
|
29
|
+
#
|
30
|
+
# key - A String or Symbol to check for existance.
|
31
|
+
#
|
32
|
+
# Returns True/False.
|
33
|
+
def include?(key)
|
34
|
+
@collection.include?(key)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Returns a value from the collection for the given key.
|
38
|
+
# If the key can't be found, there are several options:
|
39
|
+
# With no other arguments, it will raise an KeyError exception;
|
40
|
+
# if default is given, then that will be returned;
|
41
|
+
#
|
42
|
+
# key - A String or Symbol of the value to get from the collection.
|
43
|
+
# default - An optional value to be returned if the key is not found.
|
44
|
+
#
|
45
|
+
# Returns an Object.
|
46
|
+
def fetch(*args)
|
47
|
+
@collection.fetch(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Provides Hash-like access to the collection.
|
51
|
+
#
|
52
|
+
# name - A String or Symbol of the value to get from the collection.
|
53
|
+
#
|
54
|
+
# Returns an Object.
|
55
|
+
def [](name)
|
56
|
+
@collection[name.to_s]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public: Returns the wrapped collection as a Hash.
|
60
|
+
#
|
61
|
+
# Returns a Hash.
|
62
|
+
def to_h
|
63
|
+
@collection.to_hash
|
64
|
+
end
|
65
|
+
alias to_hash to_h
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
to_hash
|
69
|
+
end
|
70
|
+
|
71
|
+
# Public: Provides method access to the collection values.
|
72
|
+
#
|
73
|
+
# It allows accessing a value as `collection.name` instead of
|
74
|
+
# `collection['name']`
|
75
|
+
#
|
76
|
+
# Returns an Object.
|
77
|
+
def method_missing(method_name, *_args, &_block)
|
78
|
+
@collection.fetch(method_name.to_s) do
|
79
|
+
raise "Could not find `#{method_name}` in #{self.class.name}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Internal: Accessory method to allow the collection respond to the
|
84
|
+
# methods that will hit method_missing.
|
85
|
+
def respond_to_missing?(method_name, _include_private = false)
|
86
|
+
@collection.include?(method_name.to_s)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ApiNavigator
|
2
|
+
# Internal: Curies are named tokens that you can define in the document and use
|
3
|
+
# to express curie relation URIs in a friendlier, more compact fashion.
|
4
|
+
#
|
5
|
+
class Curie
|
6
|
+
# Public: Initializes a new Curie.
|
7
|
+
#
|
8
|
+
# curie_hash - The String with the URI of the curie.
|
9
|
+
# entry_point - The EntryPoint object to inject the configuration.
|
10
|
+
def initialize(curie_hash, entry_point)
|
11
|
+
@curie_hash = curie_hash
|
12
|
+
@entry_point = entry_point
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Indicates if the curie is an URITemplate or a regular URI.
|
16
|
+
#
|
17
|
+
# Returns true if it is templated.
|
18
|
+
# Returns false if it not templated.
|
19
|
+
def templated?
|
20
|
+
!!@curie_hash['templated']
|
21
|
+
end
|
22
|
+
|
23
|
+
# Public: Returns the name property of the Curie.
|
24
|
+
def name
|
25
|
+
@curie_hash['name']
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Returns the href property of the Curie.
|
29
|
+
def href
|
30
|
+
@curie_hash['href']
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name} #{@curie_hash}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Expands the Curie when is templated with the given variables.
|
38
|
+
#
|
39
|
+
# rel - The String rel to expand.
|
40
|
+
#
|
41
|
+
# Returns a new expanded url.
|
42
|
+
def expand(rel)
|
43
|
+
return rel unless rel && templated?
|
44
|
+
href.gsub('{rel}', rel) if href
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'faraday_middleware'
|
2
|
+
require 'faraday_hal_middleware'
|
3
|
+
require_relative '../faraday/connection'
|
4
|
+
|
5
|
+
module ApiNavigator
|
6
|
+
# Public: Exception that is raised when trying to modify an
|
7
|
+
# already initialized connection.
|
8
|
+
class ConnectionAlreadyInitializedError < StandardError
|
9
|
+
# Public: Returns a String with the exception message.
|
10
|
+
def message
|
11
|
+
'The connection has already been initialized.'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: The EntryPoint is the main public API for ApiNavigator. It is used to
|
16
|
+
# initialize an API client and setup the configuration.
|
17
|
+
#
|
18
|
+
# Examples
|
19
|
+
#
|
20
|
+
# client = ApiNavigator::EntryPoint.new('http://my.api.org')
|
21
|
+
#
|
22
|
+
# client = ApiNavigator::EntryPoint.new('http://my.api.org') do |entry_point|
|
23
|
+
# entry_point.connection do |conn|
|
24
|
+
# conn.use Faraday::Request::OAuth
|
25
|
+
# end
|
26
|
+
# entry_point.headers['Access-Token'] = 'token'
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
class EntryPoint < Link
|
30
|
+
extend Forwardable
|
31
|
+
|
32
|
+
attr_reader :client_identifier
|
33
|
+
|
34
|
+
# Public: Delegates common methods to be used with the Faraday connection.
|
35
|
+
def_delegators :connection, :basic_auth, :digest_auth, :token_auth, :params, :params=
|
36
|
+
|
37
|
+
# Public: Initializes an EntryPoint.
|
38
|
+
#
|
39
|
+
# url - A String with the entry point of your API.
|
40
|
+
def initialize(url, client_identifier=nil, &_block)
|
41
|
+
@link = { 'href' => url }
|
42
|
+
@entry_point = self
|
43
|
+
@options = {}
|
44
|
+
@connection = nil
|
45
|
+
@resource = nil
|
46
|
+
@key = nil
|
47
|
+
@uri_variables = nil
|
48
|
+
@client_identifier = client_identifier
|
49
|
+
yield self if block_given?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: A Faraday connection to use as a HTTP client.
|
53
|
+
#
|
54
|
+
# options - A Hash containing additional options to pass to Farday. Use
|
55
|
+
# {default: false} if you want to skip using default Faraday options set by
|
56
|
+
# ApiNavigator.
|
57
|
+
#
|
58
|
+
# Returns a Faraday::Connection.
|
59
|
+
def connection(options = {}, &block)
|
60
|
+
@faraday_options ||= options.dup
|
61
|
+
if block_given?
|
62
|
+
raise ConnectionAlreadyInitializedError if @connection
|
63
|
+
@faraday_block = if @faraday_options.delete(:default) == false
|
64
|
+
block
|
65
|
+
else
|
66
|
+
lambda do |conn|
|
67
|
+
default_faraday_block.call conn
|
68
|
+
yield conn
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
@connection ||= Faraday.new(_url, faraday_options, &faraday_block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Public: Headers included with every API request.
|
77
|
+
#
|
78
|
+
# Returns a Hash.
|
79
|
+
def headers
|
80
|
+
return @connection.headers if @connection
|
81
|
+
@headers ||= default_headers
|
82
|
+
end
|
83
|
+
|
84
|
+
# Public: Set headers.
|
85
|
+
#
|
86
|
+
# value - A Hash containing headers to include with every API request.
|
87
|
+
def headers=(value)
|
88
|
+
raise ConnectionAlreadyInitializedError if @connection
|
89
|
+
@headers = value
|
90
|
+
end
|
91
|
+
|
92
|
+
# Public: Options passed to Faraday
|
93
|
+
#
|
94
|
+
# Returns a Hash.
|
95
|
+
def faraday_options
|
96
|
+
(@faraday_options ||= {}).merge(headers: headers)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Public: Set Faraday connection options.
|
100
|
+
#
|
101
|
+
# value - A Hash containing options to pass to Faraday
|
102
|
+
def faraday_options=(value)
|
103
|
+
raise ConnectionAlreadyInitializedError if @connection
|
104
|
+
@faraday_options = value
|
105
|
+
end
|
106
|
+
|
107
|
+
# Public: Faraday block used with every API request.
|
108
|
+
#
|
109
|
+
# Returns a Proc.
|
110
|
+
def faraday_block
|
111
|
+
@faraday_block ||= default_faraday_block
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: Set a Faraday block to use with every API request.
|
115
|
+
#
|
116
|
+
# value - A Proc accepting a Faraday::Connection.
|
117
|
+
def faraday_block=(value)
|
118
|
+
raise ConnectionAlreadyInitializedError if @connection
|
119
|
+
@faraday_block = value
|
120
|
+
end
|
121
|
+
|
122
|
+
# Public: Read/Set options.
|
123
|
+
#
|
124
|
+
# value - A Hash containing the client options.
|
125
|
+
attr_accessor :options
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Internal: Returns a block to initialize the Faraday connection. The
|
130
|
+
# default block includes a middleware to encode requests as JSON, a
|
131
|
+
# response middleware to parse JSON responses and sets the adapter as
|
132
|
+
# NetHttp.
|
133
|
+
#
|
134
|
+
# These middleware can always be changed by accessing the Faraday
|
135
|
+
# connection.
|
136
|
+
#
|
137
|
+
# Returns a block.
|
138
|
+
def default_faraday_block
|
139
|
+
lambda do |connection|
|
140
|
+
connection.use Faraday::Response::RaiseError
|
141
|
+
connection.use FaradayMiddleware::FollowRedirects
|
142
|
+
connection.request :hal_json
|
143
|
+
connection.response :hal_json, content_type: /\bjson$/
|
144
|
+
connection.adapter :net_http
|
145
|
+
connection.options.params_encoder = Faraday::FlatParamsEncoder
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Internal: Returns the default headers to initialize the Faraday connection.
|
150
|
+
# The default headers et the Content-Type and Accept to application/json.
|
151
|
+
#
|
152
|
+
# Returns a Hash.
|
153
|
+
def default_headers
|
154
|
+
{ 'Content-Type' => 'application/hal+json', 'Accept' => 'application/hal+json,application/json' }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'uri_template'
|
2
|
+
|
3
|
+
module ApiNavigator
|
4
|
+
# Internal: The Link is used to let a Resource interact with the API.
|
5
|
+
#
|
6
|
+
class Link
|
7
|
+
# Public: Initializes a new Link.
|
8
|
+
#
|
9
|
+
# key - The key or name of the link.
|
10
|
+
# link - The String with the URI of the link.
|
11
|
+
# entry_point - The EntryPoint object to inject the configuration.
|
12
|
+
# uri_variables - The optional Hash with the variables to expand the link
|
13
|
+
# if it is templated.
|
14
|
+
def initialize(key, link, entry_point, uri_variables = nil)
|
15
|
+
@key = key
|
16
|
+
@link = link
|
17
|
+
@entry_point = entry_point
|
18
|
+
@uri_variables = uri_variables
|
19
|
+
@resource = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Indicates if the link is an URITemplate or a regular URI.
|
23
|
+
#
|
24
|
+
# Returns true if it is templated.
|
25
|
+
# Returns false if it not templated.
|
26
|
+
def _templated?
|
27
|
+
!!@link['templated']
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Expands the Link when is templated with the given variables.
|
31
|
+
#
|
32
|
+
# uri_variables - The Hash with the variables to expand the URITemplate.
|
33
|
+
#
|
34
|
+
# Returns a new Link with the expanded variables.
|
35
|
+
def _expand(uri_variables = {})
|
36
|
+
self.class.new(@key, @link, @entry_point, uri_variables)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Returns the url of the Link.
|
40
|
+
def _url
|
41
|
+
return @link['href'] unless _templated?
|
42
|
+
@url ||= _uri_template.expand(@uri_variables || {})
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Returns an array of variables from the URITemplate.
|
46
|
+
#
|
47
|
+
# Returns an empty array for regular URIs.
|
48
|
+
def _variables
|
49
|
+
_uri_template.variables
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Returns the type property of the Link
|
53
|
+
def _type
|
54
|
+
@link['type']
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Returns the name property of the Link
|
58
|
+
def _name
|
59
|
+
@link['name']
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Returns the deprecation property of the Link
|
63
|
+
def _deprecation
|
64
|
+
@link['deprecation']
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Returns the profile property of the Link
|
68
|
+
def _profile
|
69
|
+
@link['profile']
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Returns the title property of the Link
|
73
|
+
def _title
|
74
|
+
@link['title']
|
75
|
+
end
|
76
|
+
|
77
|
+
# Public: Returns the hreflang property of the Link
|
78
|
+
def _hreflang
|
79
|
+
@link['hreflang']
|
80
|
+
end
|
81
|
+
|
82
|
+
def _resource
|
83
|
+
@resource || _get
|
84
|
+
end
|
85
|
+
|
86
|
+
# Public: Returns the Resource which the Link is pointing to.
|
87
|
+
def _get
|
88
|
+
http_method(:get)
|
89
|
+
end
|
90
|
+
|
91
|
+
def _options
|
92
|
+
http_method(:options)
|
93
|
+
end
|
94
|
+
|
95
|
+
def _head
|
96
|
+
http_method(:head)
|
97
|
+
end
|
98
|
+
|
99
|
+
def _delete
|
100
|
+
http_method(:delete)
|
101
|
+
end
|
102
|
+
|
103
|
+
def _post(params = {})
|
104
|
+
http_method(:post, params)
|
105
|
+
end
|
106
|
+
|
107
|
+
def _put(params = {})
|
108
|
+
http_method(:put, params)
|
109
|
+
end
|
110
|
+
|
111
|
+
def _patch(params = {})
|
112
|
+
http_method(:patch, params)
|
113
|
+
end
|
114
|
+
|
115
|
+
def inspect
|
116
|
+
"#<#{self.class.name}(#{@key}) #{@link}>"
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
_url
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Internal: Delegate the method further down the API if the resource cannot serve it.
|
126
|
+
def method_missing(method, *args, &block)
|
127
|
+
if _resource.respond_to?(method.to_s)
|
128
|
+
_resource.send(method, *args, &block)
|
129
|
+
else
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Internal: Accessory method to allow the link respond to the
|
135
|
+
# methods that will hit method_missing.
|
136
|
+
def respond_to_missing?(method, _include_private = false)
|
137
|
+
_resource.respond_to?(method.to_s)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Internal: avoid delegating to resource
|
141
|
+
#
|
142
|
+
# #to_ary is called for implicit array coercion (such as parallel assignment
|
143
|
+
# or Array#flatten). Returning nil tells Ruby that this record is not Array-like.
|
144
|
+
def to_ary
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# Internal: Memoization for a URITemplate instance
|
149
|
+
def _uri_template
|
150
|
+
@uri_template ||= URITemplate.new(@link['href'])
|
151
|
+
end
|
152
|
+
|
153
|
+
def http_method(method, body = nil)
|
154
|
+
@resource = begin
|
155
|
+
response = @entry_point.connection.run_request(method, _url, body, nil)
|
156
|
+
Resource.from_representation(response.body, @entry_point, response)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|