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