lurch 0.1.0
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/.coveralls.yml +1 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.rubocop.yml +29 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +2 -0
- data/LICENSE.md +9 -0
- data/README.md +229 -0
- data/Rakefile +12 -0
- data/TODO.md +22 -0
- data/lib/lurch/changeset.rb +32 -0
- data/lib/lurch/client.rb +92 -0
- data/lib/lurch/collection.rb +111 -0
- data/lib/lurch/configuration.rb +14 -0
- data/lib/lurch/error.rb +13 -0
- data/lib/lurch/errors/bad_request.rb +6 -0
- data/lib/lurch/errors/conflict.rb +6 -0
- data/lib/lurch/errors/forbidden.rb +6 -0
- data/lib/lurch/errors/json_api_error.rb +29 -0
- data/lib/lurch/errors/not_found.rb +6 -0
- data/lib/lurch/errors/not_loaded.rb +13 -0
- data/lib/lurch/errors/relationship_not_loaded.rb +9 -0
- data/lib/lurch/errors/resource_not_loaded.rb +9 -0
- data/lib/lurch/errors/server_error.rb +6 -0
- data/lib/lurch/errors/unauthorized.rb +6 -0
- data/lib/lurch/errors/unprocessable_entity.rb +6 -0
- data/lib/lurch/inflector.rb +60 -0
- data/lib/lurch/logger.rb +7 -0
- data/lib/lurch/middleware/json_api_request.rb +22 -0
- data/lib/lurch/middleware/json_api_response.rb +24 -0
- data/lib/lurch/paginator.rb +71 -0
- data/lib/lurch/payload_builder.rb +43 -0
- data/lib/lurch/query.rb +143 -0
- data/lib/lurch/query_builder.rb +26 -0
- data/lib/lurch/railtie.rb +9 -0
- data/lib/lurch/relationship/has_many.rb +17 -0
- data/lib/lurch/relationship/has_one.rb +21 -0
- data/lib/lurch/relationship/linked.rb +40 -0
- data/lib/lurch/relationship.rb +26 -0
- data/lib/lurch/resource.rb +82 -0
- data/lib/lurch/store.rb +149 -0
- data/lib/lurch/store_configuration.rb +27 -0
- data/lib/lurch/stored_resource.rb +63 -0
- data/lib/lurch/uri_builder.rb +32 -0
- data/lib/lurch/version.rb +3 -0
- data/lib/lurch.rb +65 -0
- data/lurch.gemspec +26 -0
- data/lurch.gif +0 -0
- data/test/helpers/lurch_test.rb +40 -0
- data/test/helpers/response_factory.rb +193 -0
- data/test/lurch/test_configuration.rb +20 -0
- data/test/lurch/test_create_resources.rb +55 -0
- data/test/lurch/test_delete_resources.rb +27 -0
- data/test/lurch/test_errors.rb +29 -0
- data/test/lurch/test_fetch_relationships.rb +50 -0
- data/test/lurch/test_fetch_resources.rb +77 -0
- data/test/lurch/test_inflector.rb +13 -0
- data/test/lurch/test_paginated_collections.rb +125 -0
- data/test/lurch/test_queries.rb +104 -0
- data/test/lurch/test_relationship.rb +17 -0
- data/test/lurch/test_resource.rb +34 -0
- data/test/lurch/test_update_relationships.rb +53 -0
- data/test/lurch/test_update_resources.rb +56 -0
- data/test/test_helper.rb +15 -0
- metadata +235 -0
data/lib/lurch/store.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
module Lurch
|
2
|
+
class Store
|
3
|
+
def initialize(url, options = {})
|
4
|
+
@config = StoreConfiguration.new(options)
|
5
|
+
@client = Client.new(url, @config)
|
6
|
+
@store = Hash.new { |hash, key| hash[key] = {} }
|
7
|
+
end
|
8
|
+
|
9
|
+
def from(type)
|
10
|
+
query.type(type)
|
11
|
+
end
|
12
|
+
alias to from
|
13
|
+
|
14
|
+
def peek(type, id)
|
15
|
+
stored_resource = resource_from_store(type, id.to_s)
|
16
|
+
return nil if stored_resource.nil?
|
17
|
+
Resource.new(self, stored_resource.type, stored_resource.id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def save(changeset, query = {})
|
21
|
+
return insert(changeset) if changeset.id.nil?
|
22
|
+
url = uri_builder.resource_uri(changeset.type, changeset.id, query)
|
23
|
+
|
24
|
+
document = @client.patch(url, payload_builder.build(changeset))
|
25
|
+
process_document(document)
|
26
|
+
rescue Errors::JSONApiError => err
|
27
|
+
changeset.errors = err.errors
|
28
|
+
raise err
|
29
|
+
end
|
30
|
+
|
31
|
+
def insert(changeset, query = {})
|
32
|
+
return save(changeset) unless changeset.id.nil?
|
33
|
+
url = uri_builder.resources_uri(changeset.type, query)
|
34
|
+
|
35
|
+
document = @client.post(url, payload_builder.build(changeset))
|
36
|
+
process_document(document)
|
37
|
+
rescue Errors::JSONApiError => err
|
38
|
+
changeset.errors = err.errors
|
39
|
+
raise err
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(resource, query = {})
|
43
|
+
url = uri_builder.resource_uri(resource.type, resource.id, query)
|
44
|
+
@client.delete(url)
|
45
|
+
|
46
|
+
remove(resource)
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
# add resource(s) to a has many relationship
|
51
|
+
def add_related(resource, relationship_key, related_resources)
|
52
|
+
modify_relationship(:post, resource, relationship_key, related_resources)
|
53
|
+
end
|
54
|
+
|
55
|
+
# remove resource(s) from a has many relationship
|
56
|
+
def remove_related(resource, relationship_key, related_resources)
|
57
|
+
modify_relationship(:delete, resource, relationship_key, related_resources)
|
58
|
+
end
|
59
|
+
|
60
|
+
# replace resource(s) for a has many or has one relationship
|
61
|
+
def update_related(resource, relationship_key, related_resources)
|
62
|
+
modify_relationship(:patch, resource, relationship_key, related_resources)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @private
|
66
|
+
def load_from_url(url)
|
67
|
+
document = @client.get(url)
|
68
|
+
process_document(document)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @private
|
72
|
+
def resource_from_store(type, id)
|
73
|
+
normalized_type = Inflector.decode_type(type)
|
74
|
+
@store[normalized_type][id]
|
75
|
+
end
|
76
|
+
|
77
|
+
# @private
|
78
|
+
def query
|
79
|
+
Query.new(self, inflector)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def inflector
|
85
|
+
@inflector ||= Inflector.new(@config.inflection_mode, @config.types_mode)
|
86
|
+
end
|
87
|
+
|
88
|
+
def uri_builder
|
89
|
+
@uri_builder ||= URIBuilder.new(inflector)
|
90
|
+
end
|
91
|
+
|
92
|
+
def payload_builder
|
93
|
+
@payload_builder ||= PayloadBuilder.new(inflector)
|
94
|
+
end
|
95
|
+
|
96
|
+
def process_document(document)
|
97
|
+
stored_resources = store_resources(document)
|
98
|
+
resources = stored_resources.map do |stored_resource|
|
99
|
+
Resource.new(self, stored_resource.type, stored_resource.id)
|
100
|
+
end
|
101
|
+
|
102
|
+
if document["data"].is_a?(Array)
|
103
|
+
paginator = pagination_links?(document) ? Paginator.new(self, document, inflector, @config) : nil
|
104
|
+
Collection.new(resources, paginator)
|
105
|
+
else
|
106
|
+
resources.first
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def push(stored_resource)
|
111
|
+
@store[stored_resource.type][stored_resource.id] = stored_resource
|
112
|
+
end
|
113
|
+
|
114
|
+
def remove(resource)
|
115
|
+
@store[resource.type].delete(resource.id)
|
116
|
+
end
|
117
|
+
|
118
|
+
def store_resources(document)
|
119
|
+
primary_data = Lurch.to_a(document["data"])
|
120
|
+
|
121
|
+
primary_stored_resources = primary_data.map do |resource_object|
|
122
|
+
push(StoredResource.new(self, resource_object))
|
123
|
+
end
|
124
|
+
|
125
|
+
Lurch.to_a(document["included"]).each do |resource_object|
|
126
|
+
push(StoredResource.new(self, resource_object))
|
127
|
+
end
|
128
|
+
|
129
|
+
primary_stored_resources
|
130
|
+
end
|
131
|
+
|
132
|
+
def modify_relationship(method, resource, relationship_key, related_resources)
|
133
|
+
url = uri_builder.relationship_uri(resource.type, resource.id, relationship_key)
|
134
|
+
payload = payload_builder.build(related_resources, true)
|
135
|
+
@client.send(method, url, payload)
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
def pagination_links?(document)
|
140
|
+
links = document["links"]
|
141
|
+
links && (
|
142
|
+
links["first"] ||
|
143
|
+
links["last"] ||
|
144
|
+
links["next"] ||
|
145
|
+
links["prev"]
|
146
|
+
)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Lurch
|
2
|
+
class StoreConfiguration
|
3
|
+
def initialize(options)
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def authorization
|
8
|
+
@options[:authorization]
|
9
|
+
end
|
10
|
+
|
11
|
+
def inflection_mode
|
12
|
+
@options[:inflection_mode] || :dasherize
|
13
|
+
end
|
14
|
+
|
15
|
+
def types_mode
|
16
|
+
@options[:types_mode] || :pluralize
|
17
|
+
end
|
18
|
+
|
19
|
+
def pagination_record_count_key
|
20
|
+
@options[:pagination_record_count_key] || :record_count
|
21
|
+
end
|
22
|
+
|
23
|
+
def pagination_page_count_key
|
24
|
+
@options[:pagination_page_count_key] || :page_count
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Lurch
|
2
|
+
class StoredResource
|
3
|
+
def initialize(store, resource_object)
|
4
|
+
@store = store
|
5
|
+
@resource_object = resource_object
|
6
|
+
end
|
7
|
+
|
8
|
+
def id
|
9
|
+
@id ||= @resource_object["id"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
@type ||= Inflector.decode_type(@resource_object["type"])
|
14
|
+
end
|
15
|
+
|
16
|
+
def attributes
|
17
|
+
fixed_attributes
|
18
|
+
end
|
19
|
+
|
20
|
+
def relationships
|
21
|
+
fixed_relationships
|
22
|
+
end
|
23
|
+
|
24
|
+
def attribute?(name)
|
25
|
+
fixed_attributes.key?(name.to_sym)
|
26
|
+
end
|
27
|
+
|
28
|
+
def attribute(name)
|
29
|
+
fixed_attributes[name.to_sym]
|
30
|
+
end
|
31
|
+
|
32
|
+
def relationship?(name)
|
33
|
+
fixed_relationships.key?(name.to_sym)
|
34
|
+
end
|
35
|
+
|
36
|
+
def relationship(name)
|
37
|
+
fixed_relationships[name.to_sym]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def fixed_attributes
|
43
|
+
@fixed_attributes ||= resource_attributes.each_with_object({}) do |(key, value), hash|
|
44
|
+
hash[Inflector.decode_key(key)] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def fixed_relationships
|
49
|
+
@fixed_relationships ||= resource_relationships.each_with_object({}) do |(key, value), hash|
|
50
|
+
relationship_key = Inflector.decode_key(key)
|
51
|
+
hash[relationship_key] = Relationship.from_document(@store, relationship_key, value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def resource_attributes
|
56
|
+
@resource_object["attributes"] || {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def resource_relationships
|
60
|
+
@resource_object["relationships"] || []
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Lurch
|
2
|
+
class URIBuilder
|
3
|
+
def initialize(inflector)
|
4
|
+
@inflector = inflector
|
5
|
+
end
|
6
|
+
|
7
|
+
def resources_uri(type, query = "")
|
8
|
+
resource = inflector.encode_type(type)
|
9
|
+
uri = ::URI.parse(resource)
|
10
|
+
uri.query = query unless query.empty?
|
11
|
+
uri.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def resource_uri(type, id, query = "")
|
15
|
+
resource = inflector.encode_type(type)
|
16
|
+
uri = ::URI.parse("#{resource}/#{id}")
|
17
|
+
uri.query = query unless query.empty?
|
18
|
+
uri.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def relationship_uri(type, id, relationship_key)
|
22
|
+
resource = inflector.encode_type(type)
|
23
|
+
relationship = inflector.encode_key(relationship_key)
|
24
|
+
uri = ::URI.parse("#{resource}/#{id}/relationships/#{relationship}")
|
25
|
+
uri.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :inflector
|
31
|
+
end
|
32
|
+
end
|
data/lib/lurch.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
require "inflecto"
|
4
|
+
require "faraday"
|
5
|
+
require "typhoeus"
|
6
|
+
require "typhoeus/adapters/faraday"
|
7
|
+
|
8
|
+
require "lurch/configuration"
|
9
|
+
require "lurch/logger"
|
10
|
+
require "lurch/inflector"
|
11
|
+
|
12
|
+
require "lurch/middleware/json_api_request"
|
13
|
+
require "lurch/middleware/json_api_response"
|
14
|
+
|
15
|
+
require "lurch/error"
|
16
|
+
require "lurch/errors/json_api_error"
|
17
|
+
require "lurch/errors/bad_request"
|
18
|
+
require "lurch/errors/unauthorized"
|
19
|
+
require "lurch/errors/forbidden"
|
20
|
+
require "lurch/errors/not_found"
|
21
|
+
require "lurch/errors/conflict"
|
22
|
+
require "lurch/errors/unprocessable_entity"
|
23
|
+
require "lurch/errors/server_error"
|
24
|
+
require "lurch/errors/not_loaded"
|
25
|
+
require "lurch/errors/relationship_not_loaded"
|
26
|
+
require "lurch/errors/resource_not_loaded"
|
27
|
+
|
28
|
+
require "lurch/stored_resource"
|
29
|
+
require "lurch/paginator"
|
30
|
+
require "lurch/collection"
|
31
|
+
require "lurch/relationship"
|
32
|
+
require "lurch/relationship/linked"
|
33
|
+
require "lurch/relationship/has_one"
|
34
|
+
require "lurch/relationship/has_many"
|
35
|
+
require "lurch/resource"
|
36
|
+
|
37
|
+
require "lurch/uri_builder"
|
38
|
+
require "lurch/query_builder"
|
39
|
+
require "lurch/payload_builder"
|
40
|
+
require "lurch/query"
|
41
|
+
require "lurch/changeset"
|
42
|
+
require "lurch/client"
|
43
|
+
require "lurch/store_configuration"
|
44
|
+
require "lurch/store"
|
45
|
+
|
46
|
+
require "lurch/railtie" if defined?(Rails)
|
47
|
+
|
48
|
+
module Lurch
|
49
|
+
def self.to_a(value)
|
50
|
+
return [] if value.nil?
|
51
|
+
value.is_a?(Array) ? value : [value]
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.configuration
|
55
|
+
@configuration ||= Configuration.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.reset_configuration
|
59
|
+
@configuration = Configuration.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.configure
|
63
|
+
yield(configuration)
|
64
|
+
end
|
65
|
+
end
|
data/lurch.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path("../lib/lurch/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "lurch"
|
5
|
+
gem.version = Lurch::VERSION
|
6
|
+
gem.summary = "A simple Ruby JSON API client"
|
7
|
+
gem.description = "A client library for interacting with JSON API servers, based on http://jsonapi.org/ version 1.0."
|
8
|
+
gem.homepage = "https://github.com/peek-travel/lurch"
|
9
|
+
gem.authors = ["Chris Dosé"]
|
10
|
+
gem.email = "chris@peek.com"
|
11
|
+
gem.license = "MIT"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($RS)
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
|
16
|
+
gem.add_dependency("faraday", "< 1.0")
|
17
|
+
gem.add_dependency("inflecto", "~> 0.0")
|
18
|
+
gem.add_dependency("typhoeus", "< 2.0")
|
19
|
+
|
20
|
+
gem.add_development_dependency("codecov", "~> 0.1")
|
21
|
+
gem.add_development_dependency("minitest", "~> 5.9")
|
22
|
+
gem.add_development_dependency("pry", "~> 0.10")
|
23
|
+
gem.add_development_dependency("rake", "~> 12.2")
|
24
|
+
gem.add_development_dependency("rubocop", "~> 0.43")
|
25
|
+
gem.add_development_dependency("webmock", "~> 3.1")
|
26
|
+
end
|
data/lurch.gif
ADDED
Binary file
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module LurchTest
|
2
|
+
def setup
|
3
|
+
# TODO: figure out how to run test suite multiple times with different setup, so we don't randomize like this
|
4
|
+
inflection_mode = [:dasherize, :underscore].sample
|
5
|
+
types_mode = [:pluralize, :singularize].sample
|
6
|
+
|
7
|
+
@url = "http://example.com/api"
|
8
|
+
@store = Lurch::Store.new(@url, inflection_mode: inflection_mode, types_mode: types_mode)
|
9
|
+
@inflector = Lurch::Inflector.new(inflection_mode, types_mode)
|
10
|
+
@response_factory = ResponseFactory.new(@inflector, @url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def person_type
|
14
|
+
@inflector.encode_type(:person)
|
15
|
+
end
|
16
|
+
|
17
|
+
def phone_number_type
|
18
|
+
@inflector.encode_type(:phone_number)
|
19
|
+
end
|
20
|
+
|
21
|
+
def stub_get(path, response)
|
22
|
+
stub_req(:get, path, response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def stub_patch(path, response)
|
26
|
+
stub_req(:patch, path, response)
|
27
|
+
end
|
28
|
+
|
29
|
+
def stub_post(path, response)
|
30
|
+
stub_req(:post, path, response)
|
31
|
+
end
|
32
|
+
|
33
|
+
def stub_delete(path, response)
|
34
|
+
stub_req(:delete, path, response)
|
35
|
+
end
|
36
|
+
|
37
|
+
def stub_req(method, path, response)
|
38
|
+
stub_request(method, "#{@url}/#{path}").to_return(response)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
class ResponseFactory
|
2
|
+
def initialize(inflector, url)
|
3
|
+
@inflector = inflector
|
4
|
+
@url = url
|
5
|
+
end
|
6
|
+
|
7
|
+
def no_content_response
|
8
|
+
header(:no_content)
|
9
|
+
end
|
10
|
+
|
11
|
+
def unauthorized_response
|
12
|
+
header(:unauthorized) + errors_document([401, "Unauthorized"])
|
13
|
+
end
|
14
|
+
|
15
|
+
def forbidden_response
|
16
|
+
header(:forbidden) + errors_document([403, "Forbidden"])
|
17
|
+
end
|
18
|
+
|
19
|
+
def not_found_response
|
20
|
+
header(:not_found) + errors_document([404, "Not Found"])
|
21
|
+
end
|
22
|
+
|
23
|
+
def unprocessable_entity_response(message)
|
24
|
+
header(:unprocessable_entity) + errors_document([422, message])
|
25
|
+
end
|
26
|
+
|
27
|
+
def server_error_response
|
28
|
+
header(:server_error) + errors_document([500, "Internal Server Error"])
|
29
|
+
end
|
30
|
+
|
31
|
+
def person_response(id, name, phone_numbers_args = nil, code: :ok, include_phone_numbers: true)
|
32
|
+
included = phone_numbers_args && include_phone_numbers ? phone_numbers_args.map { |args| phone_number_data(*args) } : []
|
33
|
+
phone_number_ids = phone_numbers_args ? phone_numbers_args.map(&:first) : nil
|
34
|
+
header(code) + document(person(id, name, phone_number_ids), included)
|
35
|
+
end
|
36
|
+
|
37
|
+
def people_response(*params, code: :ok)
|
38
|
+
header(code) + document(people(*params))
|
39
|
+
end
|
40
|
+
|
41
|
+
def paginated_people_response(per_page, page, pages)
|
42
|
+
first = per_page * (page - 1) + 1
|
43
|
+
last = first - 1 + per_page
|
44
|
+
params = (first..last).map { |i| [i.to_s, "Person#{i}"] }
|
45
|
+
|
46
|
+
meta = {record_count: pages * per_page, page_count: pages}
|
47
|
+
|
48
|
+
links = {
|
49
|
+
first: "#{@url}/#{@inflector.encode_type(:person)}?page[number]=1&page[size]=#{per_page}",
|
50
|
+
last: "#{@url}/#{@inflector.encode_type(:person)}?page[number]=#{pages}&page[size]=#{per_page}"
|
51
|
+
}
|
52
|
+
links[:next] = "#{@url}/#{@inflector.encode_type(:person)}?page[number]=#{page + 1}&page[size]=#{per_page}" if page < pages
|
53
|
+
links[:prev] = "#{@url}/#{@inflector.encode_type(:person)}?page[number]=#{page - 1}&page[size]=#{per_page}" if page > 1
|
54
|
+
|
55
|
+
header(:ok) + document(people(*params), [], meta, links)
|
56
|
+
end
|
57
|
+
|
58
|
+
def phone_number_response(id, name, number, contact_args = nil, code: :ok)
|
59
|
+
included = contact_args ? [person_data(*contact_args)] : []
|
60
|
+
contact_id = contact_args ? contact_args.first : nil
|
61
|
+
header(code) + document(phone_number(id, name, number, contact_id), included)
|
62
|
+
end
|
63
|
+
|
64
|
+
def phone_numbers_response(*params, code: :ok)
|
65
|
+
header(code) + document(phone_numbers(*params))
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def person(*args)
|
71
|
+
data(person_data(*args))
|
72
|
+
end
|
73
|
+
|
74
|
+
def people(*params)
|
75
|
+
data(params.map { |args| person_data(*args) })
|
76
|
+
end
|
77
|
+
|
78
|
+
def person_data(id, name, phone_number_ids = nil)
|
79
|
+
{
|
80
|
+
id: id,
|
81
|
+
type: @inflector.encode_type(:person),
|
82
|
+
attributes: {
|
83
|
+
name: name,
|
84
|
+
email_address: "#{name.downcase}@example.com"
|
85
|
+
},
|
86
|
+
relationships: {
|
87
|
+
phone_numbers: if phone_number_ids
|
88
|
+
{
|
89
|
+
data: ids(:phone_number, phone_number_ids)
|
90
|
+
}
|
91
|
+
else
|
92
|
+
{
|
93
|
+
links: {
|
94
|
+
related: "#{@url}/#{@inflector.encode_type(:person)}/#{id}/#{@inflector.encode_key(:phone_numbers)}"
|
95
|
+
}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
}
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def phone_number(*args)
|
103
|
+
data(phone_number_data(*args))
|
104
|
+
end
|
105
|
+
|
106
|
+
def phone_numbers(*params)
|
107
|
+
data(params.map { |args| phone_number_data(*args) })
|
108
|
+
end
|
109
|
+
|
110
|
+
def phone_number_data(id, name, phone_number, contact_id = nil)
|
111
|
+
{
|
112
|
+
id: id,
|
113
|
+
type: @inflector.encode_type(:phone_number),
|
114
|
+
attributes: {
|
115
|
+
name: name,
|
116
|
+
phone_number: phone_number
|
117
|
+
},
|
118
|
+
relationships: {
|
119
|
+
contact: if contact_id
|
120
|
+
{
|
121
|
+
data: id(:person, contact_id)
|
122
|
+
}
|
123
|
+
else
|
124
|
+
{
|
125
|
+
links: {
|
126
|
+
related: "#{@url}/#{@inflector.encode_type(:phone_number)}/#{id}/contact"
|
127
|
+
}
|
128
|
+
}
|
129
|
+
end
|
130
|
+
}
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def id(type, id)
|
135
|
+
{type: @inflector.encode_type(type), id: id.to_s}
|
136
|
+
end
|
137
|
+
|
138
|
+
def ids(type, ids)
|
139
|
+
ids.map { |i| id(type, i) }
|
140
|
+
end
|
141
|
+
|
142
|
+
def document(doc, included = [], meta = nil, links = nil)
|
143
|
+
out = {jsonapi: {version: "1.0"}}
|
144
|
+
out[:meta] = encode_value(meta) if meta
|
145
|
+
out[:links] = links if links
|
146
|
+
out = out.merge(doc)
|
147
|
+
out[:included] = included unless included.empty?
|
148
|
+
|
149
|
+
JSON.dump(out)
|
150
|
+
end
|
151
|
+
|
152
|
+
def errors_document(*args)
|
153
|
+
JSON.dump(
|
154
|
+
errors: args.map do |(status, detail)|
|
155
|
+
{
|
156
|
+
status: status,
|
157
|
+
detail: detail
|
158
|
+
}
|
159
|
+
end
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
def data(data)
|
164
|
+
{data: encode_value(data)}
|
165
|
+
end
|
166
|
+
|
167
|
+
CODES = {
|
168
|
+
ok: "200 OK".freeze,
|
169
|
+
created: "201 Created".freeze,
|
170
|
+
no_content: "204 No Content".freeze,
|
171
|
+
unauthorized: "401 Unauthorized".freeze,
|
172
|
+
forbidden: "403 Forbidden".freeze,
|
173
|
+
not_found: "404 Not Found".freeze,
|
174
|
+
unprocessable_entity: "422 Unprocessable Entity".freeze,
|
175
|
+
server_error: "500 Internal Server Error".freeze
|
176
|
+
}.freeze
|
177
|
+
|
178
|
+
def header(code)
|
179
|
+
"HTTP/1.1 #{CODES[code]}\nContent-Type: application/vnd.api+json\n\n"
|
180
|
+
end
|
181
|
+
|
182
|
+
def encode_value(value)
|
183
|
+
if value.is_a?(Hash)
|
184
|
+
value.each_with_object({}) do |(k, v), obj|
|
185
|
+
obj[@inflector.encode_key(k)] = encode_value(v)
|
186
|
+
end
|
187
|
+
elsif value.is_a?(Array)
|
188
|
+
value.map { |v| encode_value(v) }
|
189
|
+
else
|
190
|
+
value
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
class TestConfiguration < Minitest::Test
|
4
|
+
include LurchTest
|
5
|
+
|
6
|
+
def test_configuration
|
7
|
+
assert_kind_of Logger, Lurch.configuration.logger
|
8
|
+
refute Lurch.configuration.log_payloads
|
9
|
+
|
10
|
+
Lurch.configure do |config|
|
11
|
+
config.log_payloads = true
|
12
|
+
end
|
13
|
+
|
14
|
+
assert Lurch.configuration.log_payloads
|
15
|
+
|
16
|
+
Lurch.reset_configuration
|
17
|
+
|
18
|
+
refute Lurch.configuration.log_payloads
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
class TestCreateResources < Minitest::Test
|
4
|
+
include LurchTest
|
5
|
+
|
6
|
+
def test_post_new_resource_to_server_using_store
|
7
|
+
stub_post(person_type, @response_factory.person_response("3", "Carol", code: :created))
|
8
|
+
|
9
|
+
changeset = Lurch::Changeset.new(:person, name: "Carol")
|
10
|
+
person = @store.insert(changeset)
|
11
|
+
|
12
|
+
assert_requested :post, "#{@url}/#{person_type}", body: "{\"data\":{\"type\":\"#{person_type}\",\"attributes\":{\"name\":\"Carol\"}}}"
|
13
|
+
assert_equal "3", person.id
|
14
|
+
assert_equal "Carol", person.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_post_new_resource_to_server_using_query
|
18
|
+
stub_post(person_type, @response_factory.person_response("3", "Carol", code: :created))
|
19
|
+
|
20
|
+
changeset = Lurch::Changeset.new(:person, name: "Carol")
|
21
|
+
person = @store.to(:people).insert(changeset)
|
22
|
+
|
23
|
+
assert_requested :post, "#{@url}/#{person_type}", body: "{\"data\":{\"type\":\"#{person_type}\",\"attributes\":{\"name\":\"Carol\"}}}"
|
24
|
+
assert_equal "3", person.id
|
25
|
+
assert_equal "Carol", person.name
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_post_new_resource_to_server_with_relationship_to_existing_resources
|
29
|
+
stub_get("#{phone_number_type}/1", @response_factory.phone_number_response("1", "Cell", "111-222-3344"))
|
30
|
+
stub_post(person_type, @response_factory.person_response("3", "Carol", code: :created))
|
31
|
+
|
32
|
+
phone_number = @store.from(:phone_numbers).find("1")
|
33
|
+
|
34
|
+
changeset = Lurch::Changeset.new(:person, name: "Carol")
|
35
|
+
changeset.set_related(:phone_numbers, [phone_number])
|
36
|
+
person = @store.insert(changeset)
|
37
|
+
|
38
|
+
assert_requested :post, "#{@url}/#{person_type}", body: "{\"data\":{\"type\":\"#{person_type}\",\"attributes\":{\"name\":\"Carol\"},\"relationships\":{\"#{@inflector.encode_key(:phone_numbers)}\":{\"data\":[{\"id\":\"1\",\"type\":\"#{phone_number_type}\"}]}}}}"
|
39
|
+
assert_equal "3", person.id
|
40
|
+
assert_equal "Carol", person.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_validation_errors_when_posting_new_resource
|
44
|
+
stub_post(person_type, @response_factory.unprocessable_entity_response("name - can't be blank"))
|
45
|
+
|
46
|
+
changeset = Lurch::Changeset.new(:person, name: "")
|
47
|
+
|
48
|
+
err = assert_raises(Lurch::Errors::UnprocessableEntity) { @store.insert(changeset) }
|
49
|
+
|
50
|
+
assert_requested :post, "#{@url}/#{person_type}", body: "{\"data\":{\"type\":\"#{person_type}\",\"attributes\":{\"name\":\"\"}}}"
|
51
|
+
assert_equal "422: name - can't be blank", err.message
|
52
|
+
assert_equal 422, changeset.errors.first.status
|
53
|
+
assert_equal "name - can't be blank", changeset.errors.first.detail
|
54
|
+
end
|
55
|
+
end
|