api_navigator 0.0.1
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.
- 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
|