api_navigator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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