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,63 @@
|
|
1
|
+
module ApiNavigator
|
2
|
+
# Public: A wrapper class to easily acces the links in a Resource.
|
3
|
+
#
|
4
|
+
# Examples
|
5
|
+
#
|
6
|
+
# resource.links['author']
|
7
|
+
# resource.links.author
|
8
|
+
#
|
9
|
+
class LinkCollection < CollectionHash
|
10
|
+
attr_reader :_curies
|
11
|
+
|
12
|
+
# Public: Initializes a LinkCollection.
|
13
|
+
#
|
14
|
+
# collection - The Hash with the links.
|
15
|
+
# curies - The Hash with link curies.
|
16
|
+
# entry_point - The EntryPoint object to inject the configuration.
|
17
|
+
def initialize(collection, curies, entry_point)
|
18
|
+
raise "Invalid response for LinkCollection. The response was: #{collection.inspect}" if collection && !collection.respond_to?(:collect)
|
19
|
+
|
20
|
+
@_curies = (curies || {}).reduce({}) do |hash, curie_hash|
|
21
|
+
curie = build_curie(curie_hash, entry_point)
|
22
|
+
hash.update(curie.name => curie)
|
23
|
+
end
|
24
|
+
|
25
|
+
@collection = (collection || {}).reduce({}) do |hash, (name, link)|
|
26
|
+
hash.update(name => build_link(name, link, @_curies, entry_point))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Internal: Creates links from the response hash.
|
33
|
+
#
|
34
|
+
# name - A String to identify the link's name.
|
35
|
+
# link_or_links - A Hash or an Array of hashes with the links to build.
|
36
|
+
# curies - An Array of Curies for templated links.
|
37
|
+
# entry_point - The EntryPoint object to inject the configuration.
|
38
|
+
#
|
39
|
+
# Returns a Link or an Array of Links when given an Array.
|
40
|
+
def build_link(name, link_or_links, curies, entry_point)
|
41
|
+
return unless link_or_links
|
42
|
+
|
43
|
+
if link_or_links.respond_to?(:to_ary)
|
44
|
+
link_or_links.map do |link|
|
45
|
+
build_link(name, link, curies, entry_point)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
Link.new(name, link_or_links, entry_point)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Internal: Creates a curie from the response hash.
|
53
|
+
#
|
54
|
+
# curie_hash - A Hash with the curie.
|
55
|
+
# entry_point - The EntryPoint object to inject the configuration.
|
56
|
+
#
|
57
|
+
# Returns a Link or an array of Links when given an Array.
|
58
|
+
def build_curie(curie_hash, entry_point)
|
59
|
+
Curie.new(curie_hash, entry_point)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
module ApiNavigator
|
5
|
+
# Public: Exception that is raised when passing in invalid representation data
|
6
|
+
# for the resource.
|
7
|
+
class InvalidRepresentationError < ArgumentError
|
8
|
+
attr_reader :representation
|
9
|
+
|
10
|
+
def initialize(error_description, representation)
|
11
|
+
super(error_description)
|
12
|
+
@representation = representation
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Represents a resource from your API. Its responsability is to
|
17
|
+
# ease the way you access its attributes, links and embedded resources.
|
18
|
+
class Resource
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
# Public: Returns the links of the Resource as a LinkCollection.
|
22
|
+
attr_reader :_links
|
23
|
+
|
24
|
+
# Public: Returns the response object for the HTTP request that created this
|
25
|
+
# resource, if one exists.
|
26
|
+
attr_reader :_response
|
27
|
+
|
28
|
+
# Public: Delegate all HTTP methods (get, post, put, delete, options and
|
29
|
+
# head) to its self link.
|
30
|
+
def_delegators :_self_link, :_get, :_post, :_put, :_delete, :_options, :_head
|
31
|
+
|
32
|
+
def self.from_representation(representation, entry_point, response = nil)
|
33
|
+
case (representation || {}).fetch('data', "_no_data")
|
34
|
+
when Hash
|
35
|
+
Resources::MemberResource.from_representation(representation, entry_point, response)
|
36
|
+
when Array
|
37
|
+
Resources::CollectionResource.new(representation, entry_point, response)
|
38
|
+
when "_no_data"
|
39
|
+
new(representation, entry_point, response)
|
40
|
+
else
|
41
|
+
raise InvalidRepresentationError.new("Representation has not valid data element - must be Hash or List", representation)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Initializes a Resource.
|
46
|
+
#
|
47
|
+
# representation - The hash with the HAL representation of the Resource.
|
48
|
+
# entry_point - The EntryPoint object to inject the configutation.
|
49
|
+
def initialize(representation, entry_point, response = nil)
|
50
|
+
representation = validate(representation)
|
51
|
+
links = representation['_links'] || {}
|
52
|
+
@_links = LinkCollection.new(links, links['curies'], entry_point)
|
53
|
+
@_entry_point = entry_point
|
54
|
+
@_response = response
|
55
|
+
end
|
56
|
+
|
57
|
+
def inspect
|
58
|
+
"#<#{self.class.name} self_link:#{_self_link.inspect} attributes:#{@_attributes.inspect} collection:#{@_collection.inspect}>"
|
59
|
+
end
|
60
|
+
|
61
|
+
def _success?
|
62
|
+
_response && _response.success?
|
63
|
+
end
|
64
|
+
|
65
|
+
def _status
|
66
|
+
_response && _response.status
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](name)
|
70
|
+
send(name) if respond_to?(name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch(key, *args)
|
74
|
+
return self[key] if respond_to?(key)
|
75
|
+
|
76
|
+
if args.any?
|
77
|
+
args.first
|
78
|
+
elsif block_given?
|
79
|
+
yield key
|
80
|
+
else
|
81
|
+
raise KeyError
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Internal: Ensures the received representation is a valid Hash-lookalike.
|
88
|
+
def validate(representation)
|
89
|
+
return {} unless representation
|
90
|
+
|
91
|
+
if representation.respond_to?(:to_hash)
|
92
|
+
representation.to_hash.dup
|
93
|
+
else
|
94
|
+
raise InvalidRepresentationError.new(
|
95
|
+
"Invalid representation for resource (got #{representation.class}, expected Hash). " \
|
96
|
+
"Is your web server returning JSON HAL data with a 'Content-Type: application/hal+json' header?",
|
97
|
+
representation
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Internal: Returns the self Link of the Resource. Used to handle the HTTP
|
103
|
+
# methods.
|
104
|
+
def _self_link
|
105
|
+
@_links['self']
|
106
|
+
end
|
107
|
+
|
108
|
+
# Internal: Delegate the method to various elements of the resource.
|
109
|
+
#
|
110
|
+
# This allows `api.posts` instead of `api.links.posts.resource`
|
111
|
+
# as well as api.posts(id: 1) assuming posts is a link.
|
112
|
+
def method_missing(method, *args, &block)
|
113
|
+
# if _links.respond_to?(method, include_private)
|
114
|
+
if args.any? && args.first.is_a?(Hash)
|
115
|
+
return _links.send(method, [], &block)._expand(*args)
|
116
|
+
else
|
117
|
+
return _links.send(method, *args, &block)
|
118
|
+
end
|
119
|
+
# end
|
120
|
+
|
121
|
+
super
|
122
|
+
end
|
123
|
+
|
124
|
+
# Internal: Accessory method to allow the resource respond to
|
125
|
+
# methods that will hit method_missing.
|
126
|
+
def respond_to_missing?(method, include_private = false)
|
127
|
+
_links.respond_to?(method, include_private)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ApiNavigator
|
2
|
+
module Resources
|
3
|
+
class CollectionResource < Resource
|
4
|
+
|
5
|
+
# Public: Returns the embedded resource of the Resource as a
|
6
|
+
# ResourceCollection.
|
7
|
+
attr_reader :_collection
|
8
|
+
|
9
|
+
# Public: Initializes a Resource.
|
10
|
+
#
|
11
|
+
# representation - The hash with the HAL representation of the Resource.
|
12
|
+
# entry_point - The EntryPoint object to inject the configutation.
|
13
|
+
def initialize(representation, entry_point, response = nil)
|
14
|
+
super
|
15
|
+
|
16
|
+
collection_data = representation.fetch('data')
|
17
|
+
@_collection = collection_data.map do |resource| Resources::MemberResource.from_representation(resource, entry_point) end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Internal: Delegate the method to various elements of the resource.
|
21
|
+
#
|
22
|
+
# This allows `api.posts` instead of `api.links.posts.resource`
|
23
|
+
# as well as api.posts(id: 1) assuming posts is a link.
|
24
|
+
def method_missing(method, *args, &block)
|
25
|
+
begin
|
26
|
+
@_collection.send(method, *args, &block)
|
27
|
+
rescue NoMethodError
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Internal: Accessory method to allow the resource respond to
|
33
|
+
# methods that will hit method_missing.
|
34
|
+
def respond_to_missing?(method, include_private = false)
|
35
|
+
@_collection.respond_to?(method, include_private) ||
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
end # class CollectionResource < Resource
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ApiNavigator
|
2
|
+
module Resources
|
3
|
+
class MemberResource < Resource
|
4
|
+
|
5
|
+
# Public: Returns the attributes of the Resource as Attributes.
|
6
|
+
attr_reader :_attributes
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def from_representation(representation, entry_point, response = nil)
|
10
|
+
client_identifier = entry_point.client_identifier
|
11
|
+
identifier = representation.fetch('_meta',{})['type']
|
12
|
+
resource_class = ApiNavigator.resource_class(identifier, client_identifier: client_identifier)
|
13
|
+
|
14
|
+
resource_class.new(representation, entry_point, response)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: Initializes a Resource.
|
19
|
+
#
|
20
|
+
# representation - The hash with the HAL representation of the Resource.
|
21
|
+
# entry_point - The EntryPoint object to inject the configutation.
|
22
|
+
def initialize(representation, entry_point, response = nil)
|
23
|
+
super
|
24
|
+
|
25
|
+
@_attributes = Attributes.new(representation.fetch('data'))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Internal: Delegate the method to various elements of the resource.
|
29
|
+
#
|
30
|
+
# This allows `api.posts` instead of `api.links.posts.resource`
|
31
|
+
# as well as api.posts(id: 1) assuming posts is a link.
|
32
|
+
def method_missing(method, *args, &block)
|
33
|
+
if @_attributes.include?(method.to_s)
|
34
|
+
result = @_attributes[method.to_s]
|
35
|
+
|
36
|
+
if representation = resource_representation(result)
|
37
|
+
Resource.from_representation(representation, @_entry_point)
|
38
|
+
else
|
39
|
+
result
|
40
|
+
end
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Internal: Accessory method to allow the resource respond to
|
47
|
+
# methods that will hit method_missing.
|
48
|
+
def respond_to_missing?(method, include_private = false)
|
49
|
+
@_attributes.include?(method.to_s) ||
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def resource_representation(data)
|
54
|
+
return data if (data.kind_of?(Hash) && !data["data"].nil?)
|
55
|
+
|
56
|
+
return { 'data' => data } if (data.kind_of?(Array) && resource_representation(data.first))
|
57
|
+
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
end # class MemberResource < Resource
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday/digestauth'
|
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
|
+
builder.insert(0, Faraday::Request::DigestAuth, user, password)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Fixtures
|
4
|
+
module Requests
|
5
|
+
|
6
|
+
# Fixtures::Requests.root_response
|
7
|
+
def root_response(format: :json)
|
8
|
+
response = {
|
9
|
+
_links: {
|
10
|
+
self: { "href": "/" },
|
11
|
+
books: { "href": "/books" },
|
12
|
+
filter: {
|
13
|
+
href: "/productions/1?categories={filter}",
|
14
|
+
templated: true,
|
15
|
+
},
|
16
|
+
two_links: [
|
17
|
+
{ "href": "/any_resource/1" },
|
18
|
+
{ "href": "/any_resource/2" },
|
19
|
+
],
|
20
|
+
'api:authors': { "href": "/api/authors" },
|
21
|
+
curies: [
|
22
|
+
{
|
23
|
+
name: "api",
|
24
|
+
href: "/docs/resources/{rel}",
|
25
|
+
templated: true,
|
26
|
+
}
|
27
|
+
],
|
28
|
+
null_link: nil,
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
format == :json ? response.to_json : response
|
33
|
+
end
|
34
|
+
|
35
|
+
def books_response(format = :json)
|
36
|
+
response = {
|
37
|
+
data: [
|
38
|
+
{
|
39
|
+
data: {
|
40
|
+
title: 'Book 1',
|
41
|
+
body: 'Book 1 Body',
|
42
|
+
year: 1999,
|
43
|
+
publisher: {
|
44
|
+
data: {
|
45
|
+
name: 'Manning Publisher',
|
46
|
+
},
|
47
|
+
_links: {
|
48
|
+
self: {
|
49
|
+
href: "http://localhost:3000/sample/publisher/1",
|
50
|
+
}
|
51
|
+
},
|
52
|
+
_meta: { type: "publisher" }
|
53
|
+
},
|
54
|
+
},
|
55
|
+
_links: {
|
56
|
+
self: {
|
57
|
+
href: "http://localhost:3000/sample/books/1",
|
58
|
+
},
|
59
|
+
authors: {
|
60
|
+
href: "http://localhost:3000/sample/book/1/authors",
|
61
|
+
},
|
62
|
+
publisher: {
|
63
|
+
href: "http://localhost:3000/sample/book/1/publisher",
|
64
|
+
},
|
65
|
+
},
|
66
|
+
_meta: { type: "book" }
|
67
|
+
},
|
68
|
+
{
|
69
|
+
data: {
|
70
|
+
title: 'Book 2',
|
71
|
+
body: 'Book 2 Body',
|
72
|
+
year: 1999,
|
73
|
+
publisher: {
|
74
|
+
data: {
|
75
|
+
name: 'PragBook Publisher',
|
76
|
+
},
|
77
|
+
_links: {
|
78
|
+
self: {
|
79
|
+
href: "http://localhost:3000/sample/publisher/2",
|
80
|
+
}
|
81
|
+
},
|
82
|
+
_meta: { type: "publisher" }
|
83
|
+
},
|
84
|
+
},
|
85
|
+
_links: {
|
86
|
+
self: {
|
87
|
+
href: "http://localhost:3000/sample/books/2",
|
88
|
+
},
|
89
|
+
authors: {
|
90
|
+
href: "http://localhost:3000/sample/book/2/authors",
|
91
|
+
},
|
92
|
+
publisher: {
|
93
|
+
href: "http://localhost:3000/sample/book/2/publisher",
|
94
|
+
},
|
95
|
+
},
|
96
|
+
_meta: { type: "book" }
|
97
|
+
},
|
98
|
+
],
|
99
|
+
_links: {
|
100
|
+
self: {
|
101
|
+
href: "http://localhost:3000/sample/books",
|
102
|
+
}
|
103
|
+
},
|
104
|
+
_meta: { total_number_of: 22 }
|
105
|
+
}
|
106
|
+
|
107
|
+
format == :json ? response.to_json : response
|
108
|
+
end
|
109
|
+
|
110
|
+
# singular resource
|
111
|
+
def book_response(format = :json)
|
112
|
+
response = {
|
113
|
+
data: {
|
114
|
+
title: 'Book 1',
|
115
|
+
body: 'Book 1 Body',
|
116
|
+
year: 1999,
|
117
|
+
publisher: {
|
118
|
+
data: {
|
119
|
+
name: 'Manning Publisher',
|
120
|
+
},
|
121
|
+
_links: {
|
122
|
+
self: {
|
123
|
+
href: "http://localhost:3000/sample/publisher/1",
|
124
|
+
}
|
125
|
+
},
|
126
|
+
_meta: { type: "book" }
|
127
|
+
},
|
128
|
+
authors: {
|
129
|
+
data: [
|
130
|
+
# author1,
|
131
|
+
# author2
|
132
|
+
],
|
133
|
+
_links: {},
|
134
|
+
_meta: {}
|
135
|
+
}
|
136
|
+
},
|
137
|
+
_links: {
|
138
|
+
self: {
|
139
|
+
href: "http://localhost:3000/sample/books/1",
|
140
|
+
},
|
141
|
+
authors: {
|
142
|
+
href: "http://localhost:3000/sample/book/1/authors",
|
143
|
+
},
|
144
|
+
publisher: {
|
145
|
+
href: "http://localhost:3000/sample/book/1/publisher",
|
146
|
+
},
|
147
|
+
},
|
148
|
+
_meta: { type: "book" }
|
149
|
+
}
|
150
|
+
|
151
|
+
format == :json ? response.to_json : response
|
152
|
+
end
|
153
|
+
|
154
|
+
module_function :root_response, :books_response, :book_response
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|