lurch 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|