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,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
|