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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +26 -0
  7. data/.yardopts +8 -0
  8. data/CHANGELOG.md +3 -0
  9. data/CONTRIBUTING.md +4 -0
  10. data/Dangerfile +2 -0
  11. data/Gemfile +23 -0
  12. data/Guardfile +5 -0
  13. data/LICENSE +22 -0
  14. data/README.md +5 -0
  15. data/RELEASING.md +4 -0
  16. data/Rakefile +34 -0
  17. data/UPGRADING.md +4 -0
  18. data/api_navigator.gemspec +23 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/fixtures_2.rb +253 -0
  22. data/lib/api_navigator.rb +45 -0
  23. data/lib/api_navigator/attributes.rb +20 -0
  24. data/lib/api_navigator/collection_hash.rb +90 -0
  25. data/lib/api_navigator/curie.rb +47 -0
  26. data/lib/api_navigator/entry_point.rb +157 -0
  27. data/lib/api_navigator/link.rb +160 -0
  28. data/lib/api_navigator/link_collection.rb +63 -0
  29. data/lib/api_navigator/resource.rb +130 -0
  30. data/lib/api_navigator/resources/collection_resource.rb +41 -0
  31. data/lib/api_navigator/resources/member_resource.rb +63 -0
  32. data/lib/api_navigator/version.rb +3 -0
  33. data/lib/faraday/connection.rb +17 -0
  34. data/spec/fixtures/requests.rb +157 -0
  35. data/spec/fixtures/sample.json +108 -0
  36. data/spec/lib/api_navigator/attribute_spec.rb +36 -0
  37. data/spec/lib/api_navigator/collection_hash_spec.rb +71 -0
  38. data/spec/lib/api_navigator/entry_point_spec.rb +185 -0
  39. data/spec/lib/api_navigator/link_collection_spec.rb +77 -0
  40. data/spec/lib/api_navigator/link_spec.rb +343 -0
  41. data/spec/lib/api_navigator/resource_spec.rb +368 -0
  42. data/spec/spec_helper.rb +112 -0
  43. data/spec/support/book_resource.rb +10 -0
  44. data/spec/support/request_helper.rb +8 -0
  45. 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