lurch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +29 -0
  6. data/.travis.yml +7 -0
  7. data/CHANGELOG.md +12 -0
  8. data/Gemfile +2 -0
  9. data/LICENSE.md +9 -0
  10. data/README.md +229 -0
  11. data/Rakefile +12 -0
  12. data/TODO.md +22 -0
  13. data/lib/lurch/changeset.rb +32 -0
  14. data/lib/lurch/client.rb +92 -0
  15. data/lib/lurch/collection.rb +111 -0
  16. data/lib/lurch/configuration.rb +14 -0
  17. data/lib/lurch/error.rb +13 -0
  18. data/lib/lurch/errors/bad_request.rb +6 -0
  19. data/lib/lurch/errors/conflict.rb +6 -0
  20. data/lib/lurch/errors/forbidden.rb +6 -0
  21. data/lib/lurch/errors/json_api_error.rb +29 -0
  22. data/lib/lurch/errors/not_found.rb +6 -0
  23. data/lib/lurch/errors/not_loaded.rb +13 -0
  24. data/lib/lurch/errors/relationship_not_loaded.rb +9 -0
  25. data/lib/lurch/errors/resource_not_loaded.rb +9 -0
  26. data/lib/lurch/errors/server_error.rb +6 -0
  27. data/lib/lurch/errors/unauthorized.rb +6 -0
  28. data/lib/lurch/errors/unprocessable_entity.rb +6 -0
  29. data/lib/lurch/inflector.rb +60 -0
  30. data/lib/lurch/logger.rb +7 -0
  31. data/lib/lurch/middleware/json_api_request.rb +22 -0
  32. data/lib/lurch/middleware/json_api_response.rb +24 -0
  33. data/lib/lurch/paginator.rb +71 -0
  34. data/lib/lurch/payload_builder.rb +43 -0
  35. data/lib/lurch/query.rb +143 -0
  36. data/lib/lurch/query_builder.rb +26 -0
  37. data/lib/lurch/railtie.rb +9 -0
  38. data/lib/lurch/relationship/has_many.rb +17 -0
  39. data/lib/lurch/relationship/has_one.rb +21 -0
  40. data/lib/lurch/relationship/linked.rb +40 -0
  41. data/lib/lurch/relationship.rb +26 -0
  42. data/lib/lurch/resource.rb +82 -0
  43. data/lib/lurch/store.rb +149 -0
  44. data/lib/lurch/store_configuration.rb +27 -0
  45. data/lib/lurch/stored_resource.rb +63 -0
  46. data/lib/lurch/uri_builder.rb +32 -0
  47. data/lib/lurch/version.rb +3 -0
  48. data/lib/lurch.rb +65 -0
  49. data/lurch.gemspec +26 -0
  50. data/lurch.gif +0 -0
  51. data/test/helpers/lurch_test.rb +40 -0
  52. data/test/helpers/response_factory.rb +193 -0
  53. data/test/lurch/test_configuration.rb +20 -0
  54. data/test/lurch/test_create_resources.rb +55 -0
  55. data/test/lurch/test_delete_resources.rb +27 -0
  56. data/test/lurch/test_errors.rb +29 -0
  57. data/test/lurch/test_fetch_relationships.rb +50 -0
  58. data/test/lurch/test_fetch_resources.rb +77 -0
  59. data/test/lurch/test_inflector.rb +13 -0
  60. data/test/lurch/test_paginated_collections.rb +125 -0
  61. data/test/lurch/test_queries.rb +104 -0
  62. data/test/lurch/test_relationship.rb +17 -0
  63. data/test/lurch/test_resource.rb +34 -0
  64. data/test/lurch/test_update_relationships.rb +53 -0
  65. data/test/lurch/test_update_resources.rb +56 -0
  66. data/test/test_helper.rb +15 -0
  67. metadata +235 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 41ee4ddf20b095a7e02065c4e5a7f6d4fc1d1dbc
4
+ data.tar.gz: 915497d9093ad580cc406a0c236fcd4b27029378
5
+ SHA512:
6
+ metadata.gz: 42109bc7dda4b0b912b50176a90c8c2519b1e0978a5e7588144a261cff7bd42f078c2ce368d05246d7a77d266cced02f1db37399a867fa9ca0e694200b3b466c
7
+ data.tar.gz: ee3fb80f1f7b6ca88b62ae469348356db96ebd33a3558a1b1badf038624648950ee7e27e089128f9507bdb2cb83c0ae3e0b97beefc0943e9f1f0def3c8e91b88
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-pro
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ *.gem
3
+ spec/examples.txt
4
+ coverage
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,29 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.1
3
+ Exclude:
4
+ - 'test/**/*'
5
+ - 'vendor/**/*'
6
+
7
+ Layout/AccessModifierIndentation:
8
+ EnforcedStyle: outdent
9
+
10
+ Documentation:
11
+ Enabled: false
12
+
13
+ Metrics/LineLength:
14
+ Max: 160
15
+
16
+ Style/StringLiterals:
17
+ EnforcedStyle: double_quotes
18
+
19
+ Style/DoubleNegation:
20
+ Enabled: false
21
+
22
+ Style/ParallelAssignment:
23
+ Enabled: false
24
+
25
+ Metrics/ClassLength:
26
+ Enabled: false
27
+
28
+ Metrics/MethodLength:
29
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
5
+ - 2.3
6
+ - 2.4
7
+ cache: bundler
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased][]
8
+
9
+ ## 0.1.0 - 2017-10-30
10
+ ### Initial public release
11
+
12
+ [Unreleased]: https://github.com/peek-travel/cocktail/compare/0.1.0...HEAD
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright 2017 Peek Travel, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # Lurch
2
+ [![Build Status](https://travis-ci.org/peek-travel/lurch.svg?branch=master)](https://travis-ci.org/peek-travel/lurch) [![Code Coverage](https://codecov.io/gh/peek-travel/lurch/branch/master/graph/badge.svg)](https://codecov.io/gh/peek-travel/lurch) [![Gem Version](https://img.shields.io/gem/v/lurch.svg)](https://rubygems.org/gems/lurch)
3
+
4
+ ![lurch](./lurch.gif)
5
+
6
+ A simple Ruby [JSON API](http://jsonapi.org/) client.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```
13
+ gem 'lurch'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```
19
+ $ bundle
20
+ ```
21
+
22
+ Or install it yourself:
23
+
24
+ ```
25
+ $ gem install lurch
26
+ ```
27
+
28
+ ## Basic Usage
29
+
30
+ Start by creating a store:
31
+
32
+ ```ruby
33
+ store = Lurch::Store.new("http://example.com/api")
34
+ ```
35
+
36
+ ### Fetch resources from the server
37
+
38
+ GET individual resources from the server by id:
39
+
40
+ ```ruby
41
+ person = store.from(:people).find("1")
42
+ #=> #<Lurch::Resource[Person] id: "1", name: "Bob">
43
+ ```
44
+
45
+ Or GET all of them at once:
46
+
47
+ ```ruby
48
+ people = store.from(:people).all
49
+ #=> [#<Lurch::Resource[Person] id: "1", name: "Bob">, #<Lurch::Resource[Person] id: "2", name: "Alice">]
50
+ ```
51
+
52
+ `Lurch::Resource` objects have easy accessors for all attributes returned from the server:
53
+
54
+ ```ruby
55
+ person.name
56
+ #=> "Bob"
57
+ person[:name]
58
+ #=> "Bob"
59
+ person.attributes[:name]
60
+ #=> "Bob"
61
+ ```
62
+
63
+ But, `Lurch::Resource` objects are immutable:
64
+
65
+ ```ruby
66
+ person.name = "Robert"
67
+ #=> NoMethodError: undefined method `name=' for #<Lurch::Resource:0x007fe62c848fb8>
68
+ ```
69
+
70
+ ### Update existing resources
71
+
72
+ To update an existing resource, create a changeset from the resource, then PATCH it to the server using the store:
73
+
74
+ ```ruby
75
+ changeset = Lurch::Changeset.new(person, name: "Robert")
76
+ store.save(changeset)
77
+ #=> #<Lurch::Resource[Person] id: "1", name: "Robert">
78
+ ```
79
+
80
+ Existing references to the resource will be updated:
81
+
82
+ ```ruby
83
+ person.name
84
+ #=> "Robert"
85
+ ```
86
+
87
+ ### Create new resources
88
+
89
+ To create new resources, first create a changeset, then POST it to the server using the store:
90
+
91
+ ```ruby
92
+ changeset = Lurch::Changeset.new(:person, name: "Carol")
93
+ new_person = store.insert(changeset)
94
+ #=> #<Lurch::Resource[Person] id: "3", name: "Carol">
95
+ ```
96
+
97
+ ## Filtering
98
+
99
+ You can add filters to your request if your server supports them:
100
+
101
+ ```ruby
102
+ people = store.from(:people).filter(name: "Alice").all
103
+ #=> [#<Lurch::Resource[Person] id: "2", name: "Alice">]
104
+ ```
105
+
106
+ ## Relationships
107
+
108
+ Lurch can fetch *has-many* and *has-one* relationships from the server when they are provided as *related links*:
109
+
110
+ ```ruby
111
+ person = store.from(:people).find("1")
112
+
113
+ person.hobbies
114
+ #=> #<Lurch::Relationship::Linked href: "http://example.com/api/people/1/friends">
115
+ person.hobbies.fetch
116
+ #=> #<Lurch::Collection[Hobby] size: 2, pages: 1>
117
+
118
+ person.best_friend
119
+ #=> #<Lurch::Relationship::Linked href: "http://example.com/api/people/1/best-friend">
120
+ person.best_friend.fetch
121
+ #=> #<Lurch::Resource[Person] id: "2", name: "Alice">
122
+ ```
123
+
124
+ If the server provides the relationships as *resource identifiers* instead of links, you can get some information about the relationships without having to load them:
125
+
126
+ ```ruby
127
+ person = store.from(:people).find("1")
128
+
129
+ person.hobbies
130
+ #=> [#<Lurch::Resource[Hobby] id: "1", not loaded>, ...]
131
+ person.hobbies.count
132
+ #=> 3
133
+ person.hobbies.map(&id)
134
+ #=> ["1", "2", "3"]
135
+ person.hobbies.map(&:name)
136
+ #=> Lurch::Errors::ResourceNotLoaded: Resource (Hobby) not loaded, try calling #fetch first.
137
+
138
+ person.best_friend
139
+ #=> #<Lurch::Resource[Person] id: "2", not loaded>
140
+ person.best_friend.id
141
+ #=> "2"
142
+ person.best_friend.name
143
+ #=> Lurch::Errors::ResourceNotLoaded: Resource (Person) not loaded, try calling #fetch first.
144
+ ```
145
+
146
+ Regardless of what kind of relationship it is, it can be fetched from the server:
147
+
148
+ ```ruby
149
+ person.best_friend.id
150
+ #=> "2"
151
+ person.best_friend.loaded?
152
+ #=> false
153
+ person.best_friend.fetch
154
+ #=> #<Lurch::Resource[Person] id: "2", name: "Alice">
155
+ person.best_friend.loaded?
156
+ #=> true
157
+ person.best_friend.name
158
+ #=> "Alice"
159
+ ```
160
+
161
+ ## Pagination
162
+
163
+ Lurch supports traversing and requesting paginated results if the server implements pagination:
164
+
165
+ ```ruby
166
+ people = store.from(:people).all
167
+ #=> #<Lurch::Collection[Person] size: 1000, pages: 100>
168
+ ```
169
+
170
+ If the server responded with meta data about the resources, you can get some information about them without loading them all:
171
+
172
+ ```ruby
173
+ people.size
174
+ #=> 1000
175
+ people.page_count
176
+ #=> 100
177
+ ```
178
+
179
+ *NOTE: This data comes from the top-level `meta` key in the jsonapi response document. It assumes by default the keys are "record-count" and "page-count" respectively, but can be configured in the store.*
180
+
181
+ To request a specific page, use the page query method:
182
+
183
+ ```ruby
184
+ people = store.from(:people).page(number: 12, size: 50).all
185
+ #=> #<Lurch::Collection[Person] size: 1000, pages: 20>
186
+ ```
187
+
188
+ If you'd like to traverse the whole set, you can do that using the collection enumerator or the page enumerator:
189
+
190
+ ```ruby
191
+ people.map(&:name)
192
+ # ...many HTTP requests later...
193
+ #=> ["Summer Brakus", "Katharina Orn", "Mr. Angus Hickle", "Collin Lowe PhD", "Kaylie Larson", ...]
194
+
195
+ people.each_page.map(&:size)
196
+ # ...many HTTP requests later...
197
+ #=> [10, 10, 10, 10, ...]
198
+ ```
199
+
200
+ *NOTE: These enumerators can cause many HTTP requests to the server, since when it runs out of the first page of resources, it will automatically request the next page to continue.*
201
+
202
+ *TIP: Don't use `#count` on a collection to get its size. Use `#size` instead. `#count` causes the entire collection to be traversed, whereas `#size` will try and get the information from the collection meta data.*
203
+
204
+ You can also just get the resources from the current page as an array:
205
+
206
+ ```ruby
207
+ people.resources
208
+ #=> [#<Lurch::Resource[Person] id: "2", name: "Summer Brakus", email: "summerb2b@kiehnhirthe.info", twitter: "@summerb2b">, ...]
209
+ ```
210
+
211
+ ## Authentication
212
+
213
+ You can add an *Authorization* header to all your requests by configuring the store:
214
+
215
+ ```ruby
216
+ store = Lurch::Store.new("...", authorization: "Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOjEsIm5hbWUiOiJCb2IifQ.")
217
+ ```
218
+
219
+ ## Contributing
220
+
221
+ 1. Fork it (<https://github.com/peek-travel/lurch/fork>)
222
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
223
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
224
+ 4. Push to the branch (`git push origin my-new-feature`)
225
+ 5. Create a new Pull Request
226
+
227
+ ## License
228
+
229
+ [MIT](LICENSE.md)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "rake/testtask"
2
+ require "rubocop/rake_task"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList["test/lurch/test*.rb"]
7
+ t.warning = false
8
+ end
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
data/TODO.md ADDED
@@ -0,0 +1,22 @@
1
+ # TODO list
2
+
3
+ * [x] Modify relationships using changesets
4
+ * [x] Changeset error reporting
5
+ * [x] Specify include params in a query
6
+ * [x] Specify pagination params in a query
7
+ * [x] Specify sorting params in a query
8
+ * [x] Specify sparse fields in a query
9
+ * ~~Configurable HTTP adapter (don't assume faraday with typhoeus)~~
10
+ * [x] Configurable field and path adapters (dasherized vs underscored vs ~~camelcased~~)
11
+ * [x] Configurable pluralization/singularization (don't assume urls and types are always plural)
12
+ * [x] Handle paginated results
13
+ * [ ] Allow arbitrary headers
14
+ * [ ] Allow arbitrary query params
15
+ * [ ] Singleton resources
16
+ * [ ] Handle links better?
17
+ * [x] >= 90% test coverage
18
+ * [ ] Yardoc documentation
19
+ * [x] Add license
20
+ * [x] Add changelog
21
+ * [ ] Release 0.1.0 open source
22
+ * [x] Switch to codecov.io
@@ -0,0 +1,32 @@
1
+ module Lurch
2
+ class Changeset
3
+ attr_reader :id, :type
4
+ attr_accessor :attributes, :relationships, :errors
5
+
6
+ def initialize(type, attributes = {})
7
+ is_resource = type.is_a?(Resource)
8
+ @type = is_resource ? type.type : Inflector.decode_type(type)
9
+ @id = is_resource ? type.id : nil
10
+ @attributes = attributes
11
+ @relationships = {}
12
+ end
13
+
14
+ def set(key, value)
15
+ attributes[key.to_sym] = value
16
+ self
17
+ end
18
+
19
+ def set_related(relationship_key, related_resources)
20
+ @relationships[relationship_key.to_sym] = related_resources
21
+ self
22
+ end
23
+
24
+ def inspect
25
+ attrs = ["id: #{id.inspect}"]
26
+ attrs = attrs.concat(attributes.map { |name, value| "#{name}: #{value.inspect}" })
27
+ attrs = attrs.concat(relationships.map { |name, value| "#{name}: #{value.inspect}" })
28
+ inspection = attrs.join(", ")
29
+ "#<#{self.class}[#{Inflector.classify(type)}] #{inspection}>"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,92 @@
1
+ module Lurch
2
+ class Client
3
+ AUTHORIZATION = "Authorization".freeze
4
+
5
+ STATUS_EXCEPTIONS = {
6
+ 400 => Errors::BadRequest,
7
+ 401 => Errors::Unauthorized,
8
+ 403 => Errors::Forbidden,
9
+ 404 => Errors::NotFound,
10
+ 409 => Errors::Conflict,
11
+ 422 => Errors::UnprocessableEntity,
12
+ 500 => Errors::ServerError
13
+ }.freeze
14
+
15
+ def initialize(url, config)
16
+ @url = url
17
+ @config = config
18
+ end
19
+
20
+ def get(path)
21
+ response = timed_request("GET", path) { client.get(path) }
22
+ catch_errors(response).body
23
+ end
24
+
25
+ def post(path, payload)
26
+ response = timed_request("POST", path, payload) { client.post(path, payload) }
27
+ # TODO: if 204 is returned, use payload as return body (http://jsonapi.org/format/#crud-creating-responses-204)
28
+ catch_errors(response).body
29
+ end
30
+
31
+ def patch(path, payload)
32
+ response = timed_request("PATCH", path, payload) { client.patch(path, payload) }
33
+ catch_errors(response).body
34
+ end
35
+
36
+ def delete(path, payload = nil)
37
+ response = timed_request("DELETE", path, payload) do
38
+ client.delete do |req|
39
+ req.url path
40
+ req.body = payload unless payload.nil?
41
+ end
42
+ end
43
+ catch_errors(response).body
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :url, :config
49
+
50
+ def timed_request(method, path, payload = nil)
51
+ log_request(method, path, payload)
52
+ start_time = Time.now.to_f
53
+ response = yield
54
+ end_time = Time.now.to_f
55
+ time_in_ms = ((end_time - start_time) * 1000).to_i
56
+ log_response(response, time_in_ms)
57
+ response
58
+ end
59
+
60
+ def log_request(method, path, payload)
61
+ Logger.debug { "-> #{method} #{path}" }
62
+ Logger.debug { "-> #{payload}" } if payload && Lurch.configuration.log_payloads
63
+ end
64
+
65
+ def log_response(response, time_in_ms)
66
+ Logger.debug { "<- #{response.status} in #{time_in_ms}ms" }
67
+ Logger.debug { "<- #{response.body}" } if Lurch.configuration.log_payloads
68
+ end
69
+
70
+ def catch_errors(response)
71
+ raise STATUS_EXCEPTIONS[response.status], response.body if STATUS_EXCEPTIONS[response.status]
72
+ raise Errors::ServerError, response.body unless (200..299).cover?(response.status)
73
+
74
+ response
75
+ end
76
+
77
+ def client
78
+ @client ||= Faraday.new(url: url) do |conn|
79
+ conn.headers[AUTHORIZATION] = authorization unless authorization.nil?
80
+
81
+ conn.request :jsonapi
82
+ conn.response :jsonapi
83
+
84
+ conn.adapter :typhoeus
85
+ end
86
+ end
87
+
88
+ def authorization
89
+ config.authorization
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,111 @@
1
+ module Lurch
2
+ class Collection
3
+ include Enumerable
4
+
5
+ attr_reader :resources
6
+
7
+ def initialize(resources, paginator)
8
+ @resources = resources
9
+ @paginator = paginator
10
+ end
11
+
12
+ def loaded?
13
+ true
14
+ end
15
+
16
+ def fetch
17
+ self
18
+ end
19
+
20
+ def each(&block)
21
+ block_given? ? enum.each(&block) : enum
22
+ end
23
+
24
+ def each_page(&block)
25
+ block_given? ? page_enum.each(&block) : page_enum
26
+ end
27
+
28
+ def size
29
+ @paginator ? @paginator.record_count : @resources.size
30
+ end
31
+
32
+ def page_size
33
+ @resources.size
34
+ end
35
+
36
+ def page_count
37
+ @paginator.page_count if @paginator
38
+ end
39
+
40
+ def next_collection
41
+ return @next_collection if defined?(@next_collection)
42
+ @next_collection = @paginator ? @paginator.next_collection : nil
43
+ end
44
+
45
+ def prev_collection
46
+ return @prev_collection if defined?(@prev_collection)
47
+ @prev_collection = @paginator ? @paginator.prev_collection : nil
48
+ end
49
+
50
+ def first_collection
51
+ return @first_collection if defined?(@first_collection)
52
+ @first_collection = @paginator ? @paginator.first_collection : nil
53
+ end
54
+
55
+ def last_collection
56
+ return @last_collection if defined?(@last_collection)
57
+ @last_collection = @paginator ? @paginator.last_collection : nil
58
+ end
59
+
60
+ def next?
61
+ @paginator ? @paginator.next? : false
62
+ end
63
+
64
+ def prev?
65
+ @paginator ? @paginator.prev? : false
66
+ end
67
+
68
+ def first?
69
+ @paginator ? @paginator.first? : false
70
+ end
71
+
72
+ def last?
73
+ @paginator ? @paginator.last? : false
74
+ end
75
+
76
+ def inspect
77
+ suffix = @resources.first ? "[#{Inflector.classify(@resources.first.type)}]" : ""
78
+ inspection = size ? ["size: #{size}"] : []
79
+ inspection << ["pages: #{page_count}"] if page_count
80
+ "#<#{self.class}#{suffix} #{inspection.join(', ')}>"
81
+ end
82
+
83
+ private
84
+
85
+ def enum
86
+ Enumerator.new(-> { size }) do |yielder|
87
+ @resources.each do |resource|
88
+ yielder.yield(resource)
89
+ end
90
+
91
+ if next_collection
92
+ next_collection.each do |resource|
93
+ yielder.yield(resource)
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def page_enum
100
+ Enumerator.new(-> { page_count }) do |yielder|
101
+ yielder.yield(@resources)
102
+
103
+ if next_collection
104
+ next_collection.each_page do |page|
105
+ yielder.yield(page)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,14 @@
1
+ require "logger"
2
+
3
+ module Lurch
4
+ class Configuration
5
+ attr_accessor :logger, :log_payloads
6
+
7
+ def initialize
8
+ @logger = ::Logger.new(STDOUT)
9
+ @logger.level = ::Logger::INFO
10
+
11
+ @log_payloads = false
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Lurch
2
+ class Error
3
+ JSON_API_ERROR_FIELDS = %w[id links about status code title detail source meta].freeze
4
+
5
+ attr_reader(*JSON_API_ERROR_FIELDS)
6
+
7
+ def initialize(error_object)
8
+ JSON_API_ERROR_FIELDS.each do |field|
9
+ instance_variable_set("@#{field}", error_object[field])
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Lurch
2
+ module Errors
3
+ class BadRequest < JSONApiError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Lurch
2
+ module Errors
3
+ class Conflict < JSONApiError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Lurch
2
+ module Errors
3
+ class Forbidden < JSONApiError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,29 @@
1
+ module Lurch
2
+ module Errors
3
+ class JSONApiError < StandardError
4
+ def initialize(document)
5
+ @document = document
6
+ end
7
+
8
+ def message
9
+ return @document unless errors_document?
10
+ @document["errors"].map { |error| message_from_document_error(error) }.join(", ")
11
+ end
12
+
13
+ def errors
14
+ return [] unless errors_document?
15
+ @document["errors"].map { |error| Lurch::Error.new(error) }
16
+ end
17
+
18
+ private
19
+
20
+ def errors_document?
21
+ @document.is_a?(Hash) && @document["errors"]
22
+ end
23
+
24
+ def message_from_document_error(error)
25
+ [*error["status"], *error["detail"]].join(": ")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Lurch
2
+ module Errors
3
+ class NotFound < JSONApiError
4
+ end
5
+ end
6
+ end