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,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,3 @@
1
+ module ApiNavigator
2
+ VERSION = '0.0.1'.freeze
3
+ 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