ruby_json_api_client 0.0.1 → 0.0.4
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 +4 -4
- data/README.md +130 -2
- data/lib/ruby_json_api_client/adapters/ams_adapter.rb +0 -11
- data/lib/ruby_json_api_client/adapters/rest_adapter.rb +71 -3
- data/lib/ruby_json_api_client/base.rb +55 -10
- data/lib/ruby_json_api_client/exts/ams_ext.rb +9 -0
- data/lib/ruby_json_api_client/serializers/ams_serializer.rb +23 -7
- data/lib/ruby_json_api_client/serializers/json_api_serializer.rb +3 -0
- data/lib/ruby_json_api_client/store.rb +63 -11
- data/lib/ruby_json_api_client/version.rb +1 -1
- data/lib/ruby_json_api_client.rb +0 -2
- data/perf/find_model_http_clients.rb +69 -0
- data/perf/http.rb +37 -0
- data/perf/serializer_json.rb +39 -0
- data/ruby_json_api_client.gemspec +13 -0
- data/spec/integration/ams/save_spec.rb +0 -0
- data/spec/support/classes.rb +3 -0
- data/spec/unit/base_spec.rb +40 -1
- data/spec/unit/store_spec.rb +2 -0
- metadata +78 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a316d1d7a3724fcb5dc5d292aaab71e6cda9535
|
4
|
+
data.tar.gz: 31cbe130cb74ed13c5a0eef4d75edc3f36ffc440
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 701fb12f1a1260d53ad736567576f15996ff1b8e7d423519be2aae8c5998ede272fa1950a05340863f8448df3487a3be863230a367a44d35f1f813acff2268e6
|
7
|
+
data.tar.gz: 89d0046c35a7477a9117e5ba4a369e901f69e0d5b7aa0cb507bc3758ef319ce63b856ec2032176f701b21418d5c5ac541d281ba5af26307d75ed0ee03ca78163
|
data/README.md
CHANGED
@@ -1,5 +1,39 @@
|
|
1
1
|
# RubyJsonApiClient
|
2
2
|
|
3
|
+
A library for creating models from API endpoints.
|
4
|
+
|
5
|
+
Imagine an API endpoint that looks like this
|
6
|
+
|
7
|
+
# http://www.example.com/blogs/1
|
8
|
+
{
|
9
|
+
blog: {
|
10
|
+
id: 1,
|
11
|
+
name: "A blog",
|
12
|
+
links: {
|
13
|
+
posts: "/posts?blog_id=1"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
RubyJsonApiClient allows you to write
|
19
|
+
|
20
|
+
class Blog < RubyJsonApiClient::Base
|
21
|
+
field :name
|
22
|
+
has_many :posts
|
23
|
+
end
|
24
|
+
|
25
|
+
Blog.find(1).name
|
26
|
+
# => "A blog"
|
27
|
+
|
28
|
+
Blog.find(1).posts.map(&:title)
|
29
|
+
# => ["Rails is Omakase"]
|
30
|
+
|
31
|
+
|
32
|
+
It has multiple API adapters so working with any serialization format is
|
33
|
+
easy.
|
34
|
+
|
35
|
+
#### Currently support formats
|
36
|
+
* Active Model Serializers
|
3
37
|
|
4
38
|
## Installation
|
5
39
|
|
@@ -13,13 +47,107 @@ And then execute:
|
|
13
47
|
|
14
48
|
## Usage
|
15
49
|
|
16
|
-
|
50
|
+
#### Setting up the adapter/serializer
|
51
|
+
|
52
|
+
The first thing that needs to be done is creating an adapter and
|
53
|
+
serializer. It is recommended you use one of the adapters that ships
|
54
|
+
with this gem.
|
55
|
+
|
56
|
+
For this example, we will setup an adapter that reads from
|
57
|
+
ActiveModelSerializers based APIs.
|
58
|
+
|
59
|
+
# config/initializers/ruby_json_api_client.rb
|
60
|
+
|
61
|
+
# Setup the AMS adapter to pull from https://www.example.com
|
62
|
+
RubyJsonApiClient::Store.register_adapter(:ams, {
|
63
|
+
hostname: 'www.example.com',
|
64
|
+
secure: true
|
65
|
+
});
|
66
|
+
|
67
|
+
# Use AMS based serializer
|
68
|
+
RubyJsonApiClient::Store.register_serializer(:ams)
|
69
|
+
|
70
|
+
# Default all models to AMS
|
71
|
+
RubyJsonApiClient::Store.default(:ams)
|
72
|
+
|
73
|
+
|
74
|
+
#### Models
|
75
|
+
|
76
|
+
Now you can setup your classes based on your API endpoints.
|
77
|
+
|
78
|
+
class Book < RubyJsonApiClient::Base
|
79
|
+
field :title
|
80
|
+
has_one :author
|
81
|
+
end
|
82
|
+
|
83
|
+
Book.all
|
84
|
+
# => Loads collection from https://www.example.com/books
|
85
|
+
|
86
|
+
Book.query(type: 'fiction')
|
87
|
+
# => Loads collection from https://www.example.com/books?type=fiction
|
88
|
+
|
89
|
+
Book.find(1)
|
90
|
+
# => Loads model from https://www.example.com/books/1
|
91
|
+
|
92
|
+
#### Relationships
|
93
|
+
|
94
|
+
Most relationships rules are defined based on the semantics of the
|
95
|
+
adapter you choose. For example, when using Active Model Serializers
|
96
|
+
relationships are mostly likely going to be sideloaded or accessed by
|
97
|
+
links.
|
98
|
+
|
99
|
+
class Book < RubyJsonApiClient::Base
|
100
|
+
field :title
|
101
|
+
has_one :author
|
102
|
+
end
|
103
|
+
|
104
|
+
class Author < RubyJsonApiClient::Base
|
105
|
+
field :name
|
106
|
+
has_many :books
|
107
|
+
end
|
108
|
+
|
109
|
+
With an AMS API that sideloads relationships, such as:
|
110
|
+
|
111
|
+
# http://www.example.com/books
|
112
|
+
{
|
113
|
+
books: [{
|
114
|
+
id: 1,
|
115
|
+
title: "Example book",
|
116
|
+
author_id: 123
|
117
|
+
}],
|
118
|
+
authors: [{
|
119
|
+
id: 123,
|
120
|
+
name: "Test author"
|
121
|
+
book_ids: [1]
|
122
|
+
}]
|
123
|
+
}
|
124
|
+
|
125
|
+
We could do the following.
|
126
|
+
|
127
|
+
Book.find(1).author.name
|
128
|
+
# => "Test author"
|
129
|
+
|
130
|
+
Author.find(123).books
|
131
|
+
# => [Book(<id: 1>)]
|
132
|
+
|
133
|
+
There are many more ways to load relationship data. You should consult
|
134
|
+
the adapter guide based on which adapter you are using.
|
17
135
|
|
18
136
|
## TODO
|
19
137
|
|
20
138
|
* Per model serializers and adapters (Store#adapter_for_class)
|
21
139
|
* Store#find_many_relationship should return reloadable proxy
|
140
|
+
* Store#find_single_relationship should return reloadable proxy
|
141
|
+
* Write AMS adapter docs
|
142
|
+
* Write docs for custom adapter
|
143
|
+
* Auto figure out default serializer from registered adapter(s)
|
144
|
+
* has one should accept field_id = 123. Post.find(1).author_id = 5
|
145
|
+
* Adapter/serializer should be able to add methods to models
|
146
|
+
(author_id=)
|
147
|
+
* Faraday follow redirectos
|
148
|
+
* Faraday cache
|
149
|
+
* REST handle 4xx errors
|
22
150
|
|
23
151
|
#### AMS
|
24
152
|
|
25
|
-
* serializer json to model rename data to model
|
153
|
+
* serializer: json to model rename data to model
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
2
3
|
require "addressable/uri"
|
4
|
+
require 'json'
|
3
5
|
require 'active_support'
|
4
6
|
|
5
7
|
module RubyJsonApiClient
|
@@ -9,8 +11,14 @@ module RubyJsonApiClient
|
|
9
11
|
attr_accessor :namespace
|
10
12
|
attr_accessor :port
|
11
13
|
attr_accessor :url_root
|
14
|
+
attr_accessor :http_client
|
15
|
+
attr_accessor :required_query_params
|
12
16
|
|
13
17
|
def initialize(options = {})
|
18
|
+
if options[:http_client].nil?
|
19
|
+
options[:http_client] = :net_http
|
20
|
+
end
|
21
|
+
|
14
22
|
options.each do |(field, value)|
|
15
23
|
send("#{field}=", value)
|
16
24
|
end
|
@@ -30,7 +38,24 @@ module RubyJsonApiClient
|
|
30
38
|
"#{@namespace}/#{plural.underscore}"
|
31
39
|
end
|
32
40
|
|
41
|
+
def accept_header
|
42
|
+
'application/json'
|
43
|
+
end
|
44
|
+
|
45
|
+
def user_agent
|
46
|
+
'RubyJsonApiClient'
|
47
|
+
end
|
48
|
+
|
49
|
+
def headers
|
50
|
+
{
|
51
|
+
accept: accept_header,
|
52
|
+
user_user: user_agent
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
33
56
|
def find(klass, id)
|
57
|
+
raise "Cannot find nil id" if id.nil?
|
58
|
+
|
34
59
|
path = single_path(klass, id: id)
|
35
60
|
status, _, body = http_request(:get, path, {})
|
36
61
|
|
@@ -52,13 +77,46 @@ module RubyJsonApiClient
|
|
52
77
|
end
|
53
78
|
end
|
54
79
|
|
80
|
+
def create(model, data)
|
81
|
+
url = collection_path(model.class, {})
|
82
|
+
status, _, body = http_request(:post, url, data)
|
83
|
+
|
84
|
+
if status >= 200 && status <= 299
|
85
|
+
body
|
86
|
+
else
|
87
|
+
raise "Could not post to #{url}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def update(model, data)
|
92
|
+
url = single_path(model.class, { id: model.id })
|
93
|
+
status, _, body = http_request(:put, url, data)
|
94
|
+
|
95
|
+
if status >= 200 && status <= 299
|
96
|
+
body
|
97
|
+
else
|
98
|
+
raise "Could not put to #{url}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def delete(model)
|
103
|
+
url = single_path(model.class, { id: model.id })
|
104
|
+
status, _, body = http_request(:delete, url, {})
|
105
|
+
|
106
|
+
if status >= 200 && status <= 299
|
107
|
+
body
|
108
|
+
else
|
109
|
+
raise "Could not delete to #{url}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
55
113
|
def get(url)
|
56
114
|
status, _, body = http_request(:get, url, {})
|
57
115
|
|
58
116
|
if status >= 200 && status <= 299
|
59
117
|
body
|
60
118
|
else
|
61
|
-
raise "Could not query #{
|
119
|
+
raise "Could not query #{url}"
|
62
120
|
end
|
63
121
|
end
|
64
122
|
|
@@ -69,10 +127,20 @@ module RubyJsonApiClient
|
|
69
127
|
|
70
128
|
proto = uri.scheme || (@secure ? "https" : "http")
|
71
129
|
hostname = uri.host || @hostname
|
130
|
+
port = uri.port || @port || (@secure ? 443 : 80)
|
72
131
|
path = uri.path
|
73
|
-
query_params = (uri.query_values || {}).merge(params)
|
74
132
|
|
75
|
-
|
133
|
+
query_params = (required_query_params || {})
|
134
|
+
.merge(uri.query_values || {})
|
135
|
+
.merge(params)
|
136
|
+
|
137
|
+
conn = Faraday.new("#{proto}://#{hostname}:#{port}", {
|
138
|
+
headers: headers
|
139
|
+
}) do |f|
|
140
|
+
f.request :json
|
141
|
+
f.adapter @http_client
|
142
|
+
end
|
143
|
+
|
76
144
|
response = conn.send(method, path, query_params)
|
77
145
|
[response.status, response.headers, response.body]
|
78
146
|
end
|
@@ -1,20 +1,27 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_support'
|
3
|
+
|
1
4
|
module RubyJsonApiClient
|
2
5
|
class Base
|
3
6
|
include ActiveModel::Model
|
4
7
|
include ActiveModel::AttributeMethods
|
8
|
+
include ActiveModel::Serialization
|
9
|
+
|
10
|
+
if defined?(ActiveModel::SerializerSupport)
|
11
|
+
include ActiveModel::SerializerSupport
|
12
|
+
end
|
5
13
|
|
6
14
|
def self.field(name, type = :string)
|
7
|
-
|
8
|
-
@_fields << name
|
15
|
+
fields << name
|
9
16
|
attr_accessor name
|
10
17
|
end
|
11
18
|
|
12
19
|
def self.fields
|
13
|
-
@_fields
|
20
|
+
@_fields ||= Set.new [_identifier]
|
14
21
|
end
|
15
22
|
|
16
23
|
def self.has_field?(name)
|
17
|
-
|
24
|
+
fields.include?(name)
|
18
25
|
end
|
19
26
|
|
20
27
|
def self.identifier(name)
|
@@ -33,11 +40,19 @@ module RubyJsonApiClient
|
|
33
40
|
def self.has_many(name, options = {})
|
34
41
|
@_has_many_relationships ||= []
|
35
42
|
@_has_many_relationships << name
|
43
|
+
|
36
44
|
define_method(name) do
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
45
|
+
@_loaded_has_manys ||= {}
|
46
|
+
|
47
|
+
if @_loaded_has_manys[name].nil?
|
48
|
+
result = RubyJsonApiClient::Store
|
49
|
+
.instance
|
50
|
+
.find_many_relationship(self, name, options)
|
51
|
+
|
52
|
+
@_loaded_has_manys[name] = result
|
53
|
+
end
|
54
|
+
|
55
|
+
@_loaded_has_manys[name]
|
41
56
|
end
|
42
57
|
end
|
43
58
|
|
@@ -64,6 +79,13 @@ module RubyJsonApiClient
|
|
64
79
|
@_loaded_has_ones ||= {}
|
65
80
|
@_loaded_has_ones[name] = related
|
66
81
|
end
|
82
|
+
|
83
|
+
define_method("#{name}_id=".to_sym) do |related_id|
|
84
|
+
klass_name = options[:class_name] || ActiveSupport::Inflector.classify(name)
|
85
|
+
klass = ActiveSupport::Inflector.constantize(klass_name)
|
86
|
+
@_loaded_has_ones ||= {}
|
87
|
+
@_loaded_has_ones[name] = klass.new(id: related_id)
|
88
|
+
end
|
67
89
|
end
|
68
90
|
|
69
91
|
def loaded_has_ones
|
@@ -82,6 +104,10 @@ module RubyJsonApiClient
|
|
82
104
|
RubyJsonApiClient::Store.instance.query(self, params)
|
83
105
|
end
|
84
106
|
|
107
|
+
def self.create(params)
|
108
|
+
new(params).tap(&:save)
|
109
|
+
end
|
110
|
+
|
85
111
|
def persisted?
|
86
112
|
!!send(self.class._identifier)
|
87
113
|
end
|
@@ -90,9 +116,28 @@ module RubyJsonApiClient
|
|
90
116
|
RubyJsonApiClient::Store.instance.reload(self)
|
91
117
|
end
|
92
118
|
|
93
|
-
def
|
94
|
-
|
119
|
+
def save
|
120
|
+
RubyJsonApiClient::Store.instance.save(self)
|
95
121
|
end
|
96
122
|
|
123
|
+
def update_attributes(data)
|
124
|
+
data.each do |(key, value)|
|
125
|
+
send("#{key}=", value)
|
126
|
+
end
|
127
|
+
save
|
128
|
+
end
|
129
|
+
|
130
|
+
def destroy
|
131
|
+
RubyJsonApiClient::Store.instance.delete(self)
|
132
|
+
end
|
133
|
+
|
134
|
+
def ==(other)
|
135
|
+
klass_match = (self.class == other.class)
|
136
|
+
ids_match = (send(self.class._identifier) == other.send(other.class._identifier))
|
137
|
+
|
138
|
+
klass_match && ids_match
|
139
|
+
end
|
140
|
+
alias_method :eql?, :==
|
141
|
+
alias_method :equal?, :==
|
97
142
|
end
|
98
143
|
end
|
@@ -4,20 +4,28 @@ require 'active_support'
|
|
4
4
|
module RubyJsonApiClient
|
5
5
|
class AmsSerializer
|
6
6
|
attr_accessor :store
|
7
|
+
attr_accessor :json_parsing_method
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
if options[:json_parsing_method].nil?
|
11
|
+
options[:json_parsing_method] = JSON.method(:parse)
|
12
|
+
end
|
13
|
+
|
14
|
+
options.each do |(field, value)|
|
15
|
+
send("#{field}=", value)
|
16
|
+
end
|
17
|
+
end
|
7
18
|
|
8
19
|
def transform(response)
|
9
|
-
|
20
|
+
@json_parsing_method.call(response)
|
10
21
|
end
|
11
22
|
|
12
|
-
def
|
23
|
+
def to_data(model)
|
13
24
|
key = model.class.to_s.underscore.downcase
|
25
|
+
id_field = model.class._identifier
|
14
26
|
data = {}
|
15
27
|
data[key] = {}
|
16
28
|
|
17
|
-
if model.persisted?
|
18
|
-
data[key][:id] = model.id
|
19
|
-
end
|
20
|
-
|
21
29
|
# conert fields to json
|
22
30
|
model.class.fields.reduce(data[key]) do |result, field|
|
23
31
|
result[field] = model.send(field)
|
@@ -33,7 +41,15 @@ module RubyJsonApiClient
|
|
33
41
|
result
|
34
42
|
end
|
35
43
|
|
36
|
-
|
44
|
+
if !model.persisted?
|
45
|
+
data[key].delete(id_field) if data[key].has_key?(id_field)
|
46
|
+
end
|
47
|
+
|
48
|
+
data
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_json(model)
|
52
|
+
JSON::generate(to_data(model))
|
37
53
|
end
|
38
54
|
|
39
55
|
def assert(test, failure_message)
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
1
|
module RubyJsonApiClient
|
4
2
|
class Store
|
5
3
|
def self.register_adapter(name, klass = nil, options = {})
|
@@ -17,22 +15,25 @@ module RubyJsonApiClient
|
|
17
15
|
@adapters[name] = OpenStruct.new({ klass: klass, options: options })
|
18
16
|
end
|
19
17
|
|
20
|
-
|
21
|
-
def self.register_serializer(name, klass = nil)
|
18
|
+
def self.register_serializer(name, klass = nil, options = {})
|
22
19
|
@serializers ||= {}
|
23
20
|
|
24
|
-
if
|
21
|
+
# allow for 2 arguments (automatically figure out class if so)
|
22
|
+
if !klass.is_a?(Class)
|
23
|
+
# klass is options. autoamtically figure out klass and set options
|
24
|
+
temp = klass || options
|
25
25
|
class_name = name.to_s.camelize
|
26
26
|
klass = Kernel.const_get("RubyJsonApiClient::#{class_name}Serializer")
|
27
|
+
options = temp
|
27
28
|
end
|
28
29
|
|
29
|
-
@serializers[name] = OpenStruct.new({ klass: klass })
|
30
|
+
@serializers[name] = OpenStruct.new({ klass: klass, options: options })
|
30
31
|
end
|
31
32
|
|
32
33
|
def self.get_serializer(name)
|
33
34
|
@serializers ||= {}
|
34
35
|
if @serializers[name]
|
35
|
-
@serializers[name].klass.new
|
36
|
+
@serializers[name].klass.new(@serializers[name].options)
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
@@ -114,7 +115,10 @@ module RubyJsonApiClient
|
|
114
115
|
response = parent.__origin__
|
115
116
|
|
116
117
|
# find the relationship
|
117
|
-
list = serializer.extract_many_relationship(parent, name, options, response)
|
118
|
+
list = serializer.extract_many_relationship(parent, name, options, response).map do |model|
|
119
|
+
model.__origin__ = response if model.__origin__.nil?
|
120
|
+
model
|
121
|
+
end
|
118
122
|
|
119
123
|
# wrap in enumerable proxy to allow reloading
|
120
124
|
collection = RubyJsonApiClient::Collection.new(list)
|
@@ -130,21 +134,30 @@ module RubyJsonApiClient
|
|
130
134
|
|
131
135
|
response = parent.__origin__
|
132
136
|
|
133
|
-
serializer.extract_single_relationship(parent, name, options, response)
|
137
|
+
serializer.extract_single_relationship(parent, name, options, response).tap do |model|
|
138
|
+
model.__origin__ = response if model && model.__origin__.nil?
|
139
|
+
end
|
134
140
|
end
|
135
141
|
|
142
|
+
# TODO: make the 2 following functions a bit nicer
|
136
143
|
def load_collection(klass, url)
|
137
144
|
adapter = adapter_for_class(klass)
|
138
145
|
serializer = serializer_for_class(klass)
|
139
146
|
response = adapter.get(url)
|
140
|
-
serializer.extract_many(klass, response)
|
147
|
+
serializer.extract_many(klass, response).map do |model|
|
148
|
+
model.__origin__ = response
|
149
|
+
model
|
150
|
+
end
|
141
151
|
end
|
142
152
|
|
153
|
+
# TODO: make nicer
|
143
154
|
def load_single(klass, id, url)
|
144
155
|
adapter = adapter_for_class(klass)
|
145
156
|
serializer = serializer_for_class(klass)
|
146
157
|
response = adapter.get(url)
|
147
|
-
serializer.extract_single(klass, id, response)
|
158
|
+
serializer.extract_single(klass, id, response).tap do |model|
|
159
|
+
model.__origin__ = response
|
160
|
+
end
|
148
161
|
end
|
149
162
|
|
150
163
|
def load(klass, data)
|
@@ -156,6 +169,45 @@ module RubyJsonApiClient
|
|
156
169
|
merge(model, new_model)
|
157
170
|
end
|
158
171
|
|
172
|
+
def save(model)
|
173
|
+
klass = model.class
|
174
|
+
adapter = adapter_for_class(klass)
|
175
|
+
serializer = serializer_for_class(klass)
|
176
|
+
data = serializer.to_data(model)
|
177
|
+
|
178
|
+
if model.persisted?
|
179
|
+
response = adapter.update(model, data)
|
180
|
+
else
|
181
|
+
response = adapter.create(model, data)
|
182
|
+
end
|
183
|
+
|
184
|
+
if response && response != ""
|
185
|
+
# convert response into model if there is one.
|
186
|
+
# should probably rely on adapter status (error, not changed, changed). etc
|
187
|
+
#
|
188
|
+
# TODO: can we just use serializer to load data into model?
|
189
|
+
new_model = serializer.extract_single(klass, model.id, response).tap do |m|
|
190
|
+
m.__origin__ = response
|
191
|
+
end
|
192
|
+
|
193
|
+
merge(model, new_model)
|
194
|
+
end
|
195
|
+
|
196
|
+
# TODO: Handle failures
|
197
|
+
true
|
198
|
+
end
|
199
|
+
|
200
|
+
def delete(model)
|
201
|
+
if model.persisted?
|
202
|
+
klass = model.class
|
203
|
+
adapter = adapter_for_class(klass)
|
204
|
+
adapter.delete(model)
|
205
|
+
end
|
206
|
+
|
207
|
+
# TODO: Handle failures
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
159
211
|
def merge(into, from)
|
160
212
|
into.__origin__ = from.__origin__
|
161
213
|
into.meta = from.meta
|
data/lib/ruby_json_api_client.rb
CHANGED
@@ -0,0 +1,69 @@
|
|
1
|
+
$: << '../lib/'
|
2
|
+
require 'ruby_json_api_client'
|
3
|
+
require 'typhoeus/adapters/faraday'
|
4
|
+
require 'benchmark/ips'
|
5
|
+
require 'webmock'
|
6
|
+
|
7
|
+
include WebMock::API
|
8
|
+
|
9
|
+
adapter_options = {
|
10
|
+
hostname: 'www.example.com',
|
11
|
+
namespace: 'perf',
|
12
|
+
secure: true
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
class Person < RubyJsonApiClient::Base
|
17
|
+
field :firstname
|
18
|
+
field :lastname
|
19
|
+
has_many :items
|
20
|
+
end
|
21
|
+
|
22
|
+
class Item < RubyJsonApiClient::Base
|
23
|
+
field :name
|
24
|
+
end
|
25
|
+
|
26
|
+
person_1_response = {
|
27
|
+
person: {
|
28
|
+
id: 1,
|
29
|
+
firstname: 'ryan',
|
30
|
+
lastname: 'test',
|
31
|
+
links: {
|
32
|
+
items: '/perf/items?person_id=1'
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}.to_json
|
36
|
+
|
37
|
+
stub_request(:get, "https://www.example.com/perf/people/1")
|
38
|
+
.to_return(
|
39
|
+
status: 200,
|
40
|
+
headers: { 'Content-Length' => person_1_response.size },
|
41
|
+
body: person_1_response
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
Benchmark.ips do |x|
|
46
|
+
x.report("net_http find") do
|
47
|
+
RubyJsonApiClient::Store.register_adapter(
|
48
|
+
:ams,
|
49
|
+
adapter_options.merge(http_client: :net_http)
|
50
|
+
)
|
51
|
+
RubyJsonApiClient::Store.register_serializer(:ams)
|
52
|
+
RubyJsonApiClient::Store.default(:ams)
|
53
|
+
|
54
|
+
Person.find(1)
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
x.report("typhoeus find") do
|
60
|
+
RubyJsonApiClient::Store.register_adapter(
|
61
|
+
:ams,
|
62
|
+
adapter_options.merge(http_client: :typhoeus)
|
63
|
+
)
|
64
|
+
RubyJsonApiClient::Store.register_serializer(:ams)
|
65
|
+
RubyJsonApiClient::Store.default(:ams)
|
66
|
+
|
67
|
+
Person.find(1)
|
68
|
+
end
|
69
|
+
end
|
data/perf/http.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$: << '../lib/'
|
2
|
+
require 'ruby_json_api_client'
|
3
|
+
require 'typhoeus/adapters/faraday'
|
4
|
+
require 'benchmark/ips'
|
5
|
+
require 'webmock'
|
6
|
+
|
7
|
+
include WebMock::API
|
8
|
+
|
9
|
+
class Person < RubyJsonApiClient::Base
|
10
|
+
field :firstname
|
11
|
+
field :lastname
|
12
|
+
has_many :items
|
13
|
+
end
|
14
|
+
|
15
|
+
stub_request(:get, "https://www.example.com/perf/people/1")
|
16
|
+
.to_return(
|
17
|
+
status: 200,
|
18
|
+
body: {}.to_json
|
19
|
+
)
|
20
|
+
|
21
|
+
adapter = RubyJsonApiClient::RestAdapter.new(
|
22
|
+
hostname: 'www.example.com',
|
23
|
+
secure: true,
|
24
|
+
namespace: 'perf'
|
25
|
+
)
|
26
|
+
|
27
|
+
Benchmark.ips do |x|
|
28
|
+
x.report("net http") do
|
29
|
+
adapter.http_client = :net_http
|
30
|
+
adapter.find(Person, 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
x.report("typhoeus") do
|
34
|
+
adapter.http_client = :typhoeus
|
35
|
+
adapter.find(Person, 1)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
$: << '../lib/'
|
2
|
+
require 'ruby_json_api_client'
|
3
|
+
require 'oj'
|
4
|
+
require 'yajl'
|
5
|
+
require 'benchmark/ips'
|
6
|
+
|
7
|
+
json = {
|
8
|
+
person: {
|
9
|
+
id: 1,
|
10
|
+
firstname: 'ryan',
|
11
|
+
lastname: 'test',
|
12
|
+
links: {
|
13
|
+
posts: '/posts?person_id=1'
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}.to_json
|
17
|
+
|
18
|
+
Benchmark.ips do |x|
|
19
|
+
x.report("json serializer") do
|
20
|
+
serializer = RubyJsonApiClient::AmsSerializer.new(
|
21
|
+
json_parsing_method: JSON.method(:parse)
|
22
|
+
)
|
23
|
+
serializer.transform(json)
|
24
|
+
end
|
25
|
+
|
26
|
+
x.report("oj serializer") do
|
27
|
+
serializer = RubyJsonApiClient::AmsSerializer.new(
|
28
|
+
json_parsing_method: Oj.method(:load)
|
29
|
+
)
|
30
|
+
serializer.transform(json)
|
31
|
+
end
|
32
|
+
|
33
|
+
x.report("yajl") do
|
34
|
+
serializer = RubyJsonApiClient::AmsSerializer.new(
|
35
|
+
json_parsing_method: Yajl::Parser.method(:parse)
|
36
|
+
)
|
37
|
+
serializer.transform(json)
|
38
|
+
end
|
39
|
+
end
|
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency 'json', '>= 1.8.1'
|
22
22
|
spec.add_dependency 'faraday', '>= 0.9.0'
|
23
|
+
spec.add_dependency 'faraday_middleware', '>= 0.9.0'
|
23
24
|
spec.add_dependency 'addressable', '>= 2.3.6'
|
24
25
|
spec.add_dependency 'activemodel', '>= 4.0'
|
25
26
|
spec.add_dependency 'activesupport', '>= 4.0'
|
@@ -27,8 +28,20 @@ Gem::Specification.new do |spec|
|
|
27
28
|
spec.add_development_dependency "bundler", "~> 1.6"
|
28
29
|
spec.add_development_dependency "rake"
|
29
30
|
spec.add_development_dependency "pry"
|
31
|
+
|
32
|
+
# testing
|
30
33
|
spec.add_development_dependency "rspec", "~> 3.0.0"
|
31
34
|
spec.add_development_dependency 'rspec-collection_matchers', '~> 1.0.0'
|
32
35
|
spec.add_development_dependency 'rspec-its', '~> 1.0.1'
|
33
36
|
spec.add_development_dependency 'webmock', '~> 1.18.0'
|
37
|
+
|
38
|
+
# perf
|
39
|
+
spec.add_development_dependency 'benchmark-ips', '~> 2.0.0'
|
40
|
+
|
41
|
+
# http adapters (for perf testing)
|
42
|
+
spec.add_development_dependency 'typhoeus', '~> 0.6.9'
|
43
|
+
|
44
|
+
# json parsers (for perf testing)
|
45
|
+
spec.add_development_dependency 'oj', '~> 2.10.0'
|
46
|
+
spec.add_development_dependency 'yajl-ruby', '~> 1.2.1'
|
34
47
|
end
|
File without changes
|
data/spec/support/classes.rb
CHANGED
data/spec/unit/base_spec.rb
CHANGED
@@ -6,7 +6,6 @@ describe RubyJsonApiClient::Base do
|
|
6
6
|
validates :firstname, presence: true
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
9
|
describe :field do
|
11
10
|
it "should setup attributes for the model" do
|
12
11
|
person = Person.new
|
@@ -27,6 +26,18 @@ describe RubyJsonApiClient::Base do
|
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
29
|
+
describe :has_field? do
|
30
|
+
context "a class with no fields" do
|
31
|
+
subject { Nothing.has_field?(:nope) }
|
32
|
+
it { should eq(false) }
|
33
|
+
end
|
34
|
+
|
35
|
+
context "a class with fields that has the field" do
|
36
|
+
subject { Item.has_field?(:name) }
|
37
|
+
it { should eq(true) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
30
41
|
describe :identifer do
|
31
42
|
it "should default to id" do
|
32
43
|
expect(Person._identifier).to eq :id
|
@@ -57,4 +68,32 @@ describe RubyJsonApiClient::Base do
|
|
57
68
|
expect(Person.new.persisted?).to eql(false)
|
58
69
|
end
|
59
70
|
end
|
71
|
+
|
72
|
+
describe :== do
|
73
|
+
context "two objects of different classes" do
|
74
|
+
subject { Person.new(id: 1) == Item.new(id: 1) }
|
75
|
+
it { should eq(false) }
|
76
|
+
end
|
77
|
+
|
78
|
+
context "two objects of the same class but different ids" do
|
79
|
+
subject { Person.new(id: 1) == Person.new(id: 2) }
|
80
|
+
it { should eq(false) }
|
81
|
+
end
|
82
|
+
|
83
|
+
context "two objects with the same klass and id" do
|
84
|
+
subject { Person.new(id: 1) == Person.new(id: 1) }
|
85
|
+
it { should eq(true) }
|
86
|
+
end
|
87
|
+
|
88
|
+
context "the same instance" do
|
89
|
+
let(:person) { Person.new(id: 1) }
|
90
|
+
subject { person == person }
|
91
|
+
it { should eq(true) }
|
92
|
+
end
|
93
|
+
|
94
|
+
context "two objects with non standard identifiers" do
|
95
|
+
subject { Thing.new(uuid: 'x') == Thing.new(uuid: 'x') }
|
96
|
+
it { should eq(true) }
|
97
|
+
end
|
98
|
+
end
|
60
99
|
end
|
data/spec/unit/store_spec.rb
CHANGED
@@ -184,6 +184,7 @@ describe RubyJsonApiClient::Store do
|
|
184
184
|
|
185
185
|
expect(serializer).to receive(:extract_many)
|
186
186
|
.with(Person, "[]")
|
187
|
+
.and_return([])
|
187
188
|
|
188
189
|
store.load_collection(Person, "http://example.com/testings")
|
189
190
|
end
|
@@ -205,6 +206,7 @@ describe RubyJsonApiClient::Store do
|
|
205
206
|
|
206
207
|
expect(serializer).to receive(:extract_single)
|
207
208
|
.with(Person, 1, "{}")
|
209
|
+
.and_return(OpenStruct.new)
|
208
210
|
|
209
211
|
store.load_single(Person, 1, "http://example.com/testings/1")
|
210
212
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_json_api_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Toronto
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-09-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 0.9.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday_middleware
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.9.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: addressable
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,6 +192,62 @@ dependencies:
|
|
178
192
|
- - "~>"
|
179
193
|
- !ruby/object:Gem::Version
|
180
194
|
version: 1.18.0
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: benchmark-ips
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 2.0.0
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 2.0.0
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: typhoeus
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: 0.6.9
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: 0.6.9
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: oj
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - "~>"
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: 2.10.0
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - "~>"
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: 2.10.0
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
name: yajl-ruby
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - "~>"
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: 1.2.1
|
244
|
+
type: :development
|
245
|
+
prerelease: false
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
247
|
+
requirements:
|
248
|
+
- - "~>"
|
249
|
+
- !ruby/object:Gem::Version
|
250
|
+
version: 1.2.1
|
181
251
|
description: API client for activemodel instances
|
182
252
|
email:
|
183
253
|
- ryanto@gmail.com
|
@@ -196,10 +266,14 @@ files:
|
|
196
266
|
- lib/ruby_json_api_client/adapters/rest_adapter.rb
|
197
267
|
- lib/ruby_json_api_client/base.rb
|
198
268
|
- lib/ruby_json_api_client/collection.rb
|
269
|
+
- lib/ruby_json_api_client/exts/ams_ext.rb
|
199
270
|
- lib/ruby_json_api_client/serializers/ams_serializer.rb
|
200
271
|
- lib/ruby_json_api_client/serializers/json_api_serializer.rb
|
201
272
|
- lib/ruby_json_api_client/store.rb
|
202
273
|
- lib/ruby_json_api_client/version.rb
|
274
|
+
- perf/find_model_http_clients.rb
|
275
|
+
- perf/http.rb
|
276
|
+
- perf/serializer_json.rb
|
203
277
|
- ruby_json_api_client.gemspec
|
204
278
|
- spec/integration/ams/find_spec.rb
|
205
279
|
- spec/integration/ams/has_many_links_spec.rb
|
@@ -207,6 +281,7 @@ files:
|
|
207
281
|
- spec/integration/ams/has_one_links_spec.rb
|
208
282
|
- spec/integration/ams/has_one_sideload_spec.rb
|
209
283
|
- spec/integration/ams/query_spec.rb
|
284
|
+
- spec/integration/ams/save_spec.rb
|
210
285
|
- spec/integration/json_api/find_spec.rb
|
211
286
|
- spec/integration/json_api/query_spec.rb
|
212
287
|
- spec/spec_helper.rb
|
@@ -249,6 +324,7 @@ test_files:
|
|
249
324
|
- spec/integration/ams/has_one_links_spec.rb
|
250
325
|
- spec/integration/ams/has_one_sideload_spec.rb
|
251
326
|
- spec/integration/ams/query_spec.rb
|
327
|
+
- spec/integration/ams/save_spec.rb
|
252
328
|
- spec/integration/json_api/find_spec.rb
|
253
329
|
- spec/integration/json_api/query_spec.rb
|
254
330
|
- spec/spec_helper.rb
|