ruby_json_api_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +25 -0
  6. data/Rakefile +2 -0
  7. data/lib/ruby_json_api_client/adapters/ams_adapter.rb +15 -0
  8. data/lib/ruby_json_api_client/adapters/json_api_adapter.rb +15 -0
  9. data/lib/ruby_json_api_client/adapters/rest_adapter.rb +80 -0
  10. data/lib/ruby_json_api_client/base.rb +98 -0
  11. data/lib/ruby_json_api_client/collection.rb +13 -0
  12. data/lib/ruby_json_api_client/serializers/ams_serializer.rb +187 -0
  13. data/lib/ruby_json_api_client/serializers/json_api_serializer.rb +93 -0
  14. data/lib/ruby_json_api_client/store.rb +171 -0
  15. data/lib/ruby_json_api_client/version.rb +3 -0
  16. data/lib/ruby_json_api_client.rb +14 -0
  17. data/ruby_json_api_client.gemspec +34 -0
  18. data/spec/integration/ams/find_spec.rb +46 -0
  19. data/spec/integration/ams/has_many_links_spec.rb +161 -0
  20. data/spec/integration/ams/has_many_sideload_spec.rb +170 -0
  21. data/spec/integration/ams/has_one_links_spec.rb +57 -0
  22. data/spec/integration/ams/has_one_sideload_spec.rb +87 -0
  23. data/spec/integration/ams/query_spec.rb +65 -0
  24. data/spec/integration/json_api/find_spec.rb +44 -0
  25. data/spec/integration/json_api/query_spec.rb +65 -0
  26. data/spec/spec_helper.rb +7 -0
  27. data/spec/support/classes.rb +26 -0
  28. data/spec/unit/adapters/json_api_spec.rb +4 -0
  29. data/spec/unit/adapters/rest_spec.rb +109 -0
  30. data/spec/unit/base_spec.rb +60 -0
  31. data/spec/unit/collection_spec.rb +17 -0
  32. data/spec/unit/serializers/ams_spec.rb +480 -0
  33. data/spec/unit/serializers/json_api_spec.rb +114 -0
  34. data/spec/unit/store_spec.rb +243 -0
  35. metadata +262 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cf21703bffbc99abd22c2bef3825db7ff9f67f4c
4
+ data.tar.gz: 49b8168022402b4fb3181b47826b8155f92e509d
5
+ SHA512:
6
+ metadata.gz: 5e9219c09fe7bb70c03cd6d8587bf1000e2fe5da981e7eae5142dabded8823daf34810a686f2a8c1acac3b0c21b3ab61806e8e18f461eca998eca33d5455d991
7
+ data.tar.gz: fcf2012a06ac3b1766d99ec2d56cf6717a740ac1ce41de60d31953b3dc7e708c89f320cb528a1b623811b4c40b70197fec152d689a93256395a6010aa3c354df
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ruby_json_api_client.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ryan Toronto
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # RubyJsonApiClient
2
+
3
+
4
+ ## Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ gem 'ruby_json_api_client'
9
+
10
+ And then execute:
11
+
12
+ $ bundle
13
+
14
+ ## Usage
15
+
16
+ TODO: Write usage instructions here
17
+
18
+ ## TODO
19
+
20
+ * Per model serializers and adapters (Store#adapter_for_class)
21
+ * Store#find_many_relationship should return reloadable proxy
22
+
23
+ #### AMS
24
+
25
+ * serializer json to model rename data to model
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,15 @@
1
+ module RubyJsonApiClient
2
+ class AmsAdapter < RubyJsonApiClient::RestAdapter
3
+ def create(model)
4
+
5
+ end
6
+
7
+ def update(model)
8
+
9
+ end
10
+
11
+ def destroy(model)
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module RubyJsonApiClient
2
+ class JsonApiAdapter < RubyJsonApiClient::RestAdapter
3
+ def create(model)
4
+
5
+ end
6
+
7
+ def update(model)
8
+
9
+ end
10
+
11
+ def destroy(model)
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,80 @@
1
+ require 'faraday'
2
+ require "addressable/uri"
3
+ require 'active_support'
4
+
5
+ module RubyJsonApiClient
6
+ class RestAdapter
7
+ attr_accessor :secure
8
+ attr_accessor :hostname
9
+ attr_accessor :namespace
10
+ attr_accessor :port
11
+ attr_accessor :url_root
12
+
13
+ def initialize(options = {})
14
+ options.each do |(field, value)|
15
+ send("#{field}=", value)
16
+ end
17
+ end
18
+
19
+ def single_path(klass, params = {})
20
+ name = klass.name
21
+ plural = ActiveSupport::Inflector.pluralize(name)
22
+ path = plural.underscore
23
+ id = params[:id]
24
+ "#{@namespace}/#{path}/#{id}"
25
+ end
26
+
27
+ def collection_path(klass, params)
28
+ name = klass.name
29
+ plural = ActiveSupport::Inflector.pluralize(name)
30
+ "#{@namespace}/#{plural.underscore}"
31
+ end
32
+
33
+ def find(klass, id)
34
+ path = single_path(klass, id: id)
35
+ status, _, body = http_request(:get, path, {})
36
+
37
+ if status >= 200 && status <= 299
38
+ body
39
+ else
40
+ raise "Could not find #{klass.name} with id #{id}"
41
+ end
42
+ end
43
+
44
+ def find_many(klass, params)
45
+ path = collection_path(klass, params)
46
+ status, _, body = http_request(:get, path, params)
47
+
48
+ if status >= 200 && status <= 299
49
+ body
50
+ else
51
+ raise "Could not query #{klass.name}"
52
+ end
53
+ end
54
+
55
+ def get(url)
56
+ status, _, body = http_request(:get, url, {})
57
+
58
+ if status >= 200 && status <= 299
59
+ body
60
+ else
61
+ raise "Could not query #{path}"
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ def http_request(method, url, params)
68
+ uri = Addressable::URI.parse(url)
69
+
70
+ proto = uri.scheme || (@secure ? "https" : "http")
71
+ hostname = uri.host || @hostname
72
+ path = uri.path
73
+ query_params = (uri.query_values || {}).merge(params)
74
+
75
+ conn = Faraday.new("#{proto}://#{hostname}")
76
+ response = conn.send(method, path, query_params)
77
+ [response.status, response.headers, response.body]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,98 @@
1
+ module RubyJsonApiClient
2
+ class Base
3
+ include ActiveModel::Model
4
+ include ActiveModel::AttributeMethods
5
+
6
+ def self.field(name, type = :string)
7
+ @_fields ||= []
8
+ @_fields << name
9
+ attr_accessor name
10
+ end
11
+
12
+ def self.fields
13
+ @_fields || []
14
+ end
15
+
16
+ def self.has_field?(name)
17
+ (@_fields | [_identifier]).include?(name)
18
+ end
19
+
20
+ def self.identifier(name)
21
+ @_identifier = name
22
+ field(name, :number)
23
+ end
24
+
25
+ def self._identifier
26
+ @_identifier || superclass._identifier
27
+ end
28
+
29
+ identifier :id
30
+ attr_accessor :meta
31
+ attr_accessor :__origin__
32
+
33
+ def self.has_many(name, options = {})
34
+ @_has_many_relationships ||= []
35
+ @_has_many_relationships << name
36
+ define_method(name) do
37
+ # make cachable
38
+ RubyJsonApiClient::Store
39
+ .instance
40
+ .find_many_relationship(self, name, options)
41
+ end
42
+ end
43
+
44
+ def self.has_many_relationships
45
+ @_has_many_relationships
46
+ end
47
+
48
+ def self.has_one(name, options = {})
49
+ define_method(name) do
50
+ @_loaded_has_ones ||= {}
51
+
52
+ if @_loaded_has_ones[name].nil?
53
+ result = RubyJsonApiClient::Store
54
+ .instance
55
+ .find_single_relationship(self, name, options)
56
+
57
+ @_loaded_has_ones[name] = result
58
+ end
59
+
60
+ @_loaded_has_ones[name]
61
+ end
62
+
63
+ define_method("#{name}=".to_sym) do |related|
64
+ @_loaded_has_ones ||= {}
65
+ @_loaded_has_ones[name] = related
66
+ end
67
+ end
68
+
69
+ def loaded_has_ones
70
+ @_loaded_has_ones || {}
71
+ end
72
+
73
+ def self.find(id)
74
+ RubyJsonApiClient::Store.instance.find(self, id)
75
+ end
76
+
77
+ def self.all
78
+ where({})
79
+ end
80
+
81
+ def self.where(params)
82
+ RubyJsonApiClient::Store.instance.query(self, params)
83
+ end
84
+
85
+ def persisted?
86
+ !!send(self.class._identifier)
87
+ end
88
+
89
+ def reload
90
+ RubyJsonApiClient::Store.instance.reload(self)
91
+ end
92
+
93
+ def links
94
+ store.find(self.class._identifier).__data__
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,13 @@
1
+ module RubyJsonApiClient
2
+ class Collection < BasicObject
3
+ attr_accessor :__origin__
4
+
5
+ def initialize(list)
6
+ @list = list
7
+ end
8
+
9
+ def method_missing(name, *args, &blk)
10
+ @list.send(name, *args, &blk)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,187 @@
1
+ require 'json'
2
+ require 'active_support'
3
+
4
+ module RubyJsonApiClient
5
+ class AmsSerializer
6
+ attr_accessor :store
7
+
8
+ def transform(response)
9
+ JSON.parse(response)
10
+ end
11
+
12
+ def to_json(model)
13
+ key = model.class.to_s.underscore.downcase
14
+ data = {}
15
+ data[key] = {}
16
+
17
+ if model.persisted?
18
+ data[key][:id] = model.id
19
+ end
20
+
21
+ # conert fields to json
22
+ model.class.fields.reduce(data[key]) do |result, field|
23
+ result[field] = model.send(field)
24
+ result
25
+ end
26
+
27
+ # convert has one relationships to json
28
+ relationships = model.loaded_has_ones || {}
29
+ relationships.reduce(data[key]) do |result, (name, relationship)|
30
+ if relationship.id
31
+ result["#{name}_id"] = relationship.id
32
+ end
33
+ result
34
+ end
35
+
36
+ JSON::generate(data)
37
+ end
38
+
39
+ def assert(test, failure_message)
40
+ raise failure_message if !test
41
+ end
42
+
43
+ def _create_model(klass, data)
44
+ model = klass.new(meta: {})
45
+
46
+ model.meta[:data] = data
47
+
48
+ if data['links']
49
+ model.meta[:links] = data['links']
50
+ end
51
+
52
+ data.reduce(model) do |record, (field, value)|
53
+ if klass.has_field?(field.to_sym)
54
+ record.send("#{field}=", value)
55
+ end
56
+
57
+ record
58
+ end
59
+ end
60
+
61
+ def extract_single(klass, id, response)
62
+ name = klass.to_s.underscore
63
+ data = transform(response)
64
+
65
+ assert data[name],
66
+ "No key #{name} in json response."
67
+
68
+ assert data[name]['id'],
69
+ "No id included in #{name} json data"
70
+
71
+ if id
72
+ # we will allow idless loading, but if an id is given we
73
+ # will try to verify it.
74
+ assert data[name]['id'].to_s == id.to_s,
75
+ "Tried to find #{name} with id #{id}, but got #{name} with id #{data[name]['id']}."
76
+ end
77
+
78
+ _create_model(klass, data[name])
79
+ end
80
+
81
+ def extract_many(klass, response, key = nil)
82
+ key = klass.to_s.underscore if key.nil?
83
+ plural = ActiveSupport::Inflector.pluralize(key)
84
+
85
+ data = transform(response)
86
+
87
+ assert data[plural],
88
+ "No key #{plural} in json response."
89
+
90
+ assert data[plural].is_a?(Array),
91
+ "Key #{plural} should be an array"
92
+
93
+ data[plural].reduce([]) do |collection, json|
94
+ collection << _create_model(klass, json)
95
+ end
96
+ end
97
+
98
+ def extract_many_relationship(parent, name, options, response)
99
+ # given response this will find the relationship
100
+ # for ams based apis the relationship will either be
101
+ # 1) in links
102
+ # 2) in sideloaded data
103
+ data = transform(response)
104
+ singular = ActiveSupport::Inflector.singularize(name)
105
+ meta = parent.meta || {}
106
+ meta_links = meta[:links]
107
+ meta_data = meta[:data]
108
+
109
+ if meta_links && meta_links[name.to_s]
110
+ extract_many_relationship_from_links(parent, name, options, meta_links[name.to_s])
111
+
112
+ elsif data[name.to_s] && meta_data && meta_data["#{singular}_ids"]
113
+ extract_many_relationship_from_sideload(parent, name, options, response)
114
+
115
+ else
116
+ []
117
+
118
+ end
119
+ end
120
+
121
+ def extract_many_relationship_from_links(parent, name, options, url)
122
+ # since we only have a url pointing to where to pull
123
+ # this info from we need to go back to the store and
124
+ # have it pull this data
125
+ klass_name = options[:class_name] || ActiveSupport::Inflector.classify(name)
126
+ klass = ActiveSupport::Inflector.constantize(klass_name)
127
+
128
+ store.load_collection(klass, url)
129
+ end
130
+
131
+ def extract_many_relationship_from_sideload(parent, name, options, response)
132
+ singular = ActiveSupport::Inflector.singularize(name)
133
+ plural = ActiveSupport::Inflector.pluralize(name)
134
+ klass_name = options[:class_name] || ActiveSupport::Inflector.classify(name)
135
+ klass = ActiveSupport::Inflector.constantize(klass_name)
136
+ meta_data = parent.meta[:data]
137
+
138
+ ids = meta_data["#{singular}_ids"]
139
+ idMap = ids.reduce({}) do |map, id|
140
+ map[id] = true
141
+ map
142
+ end
143
+
144
+ extract_many(klass, response, plural)
145
+ .select { |record| idMap[record.id] }
146
+ end
147
+
148
+ def extract_single_relationship(parent, name, options, response)
149
+ plural = ActiveSupport::Inflector.pluralize(name)
150
+ data = transform(response)
151
+ meta = parent.meta || {}
152
+ meta_links = meta[:links]
153
+ meta_data = meta[:data]
154
+
155
+ if meta_links && meta_links[name.to_s]
156
+ extract_single_relationship_from_links(parent, name, options, meta_links[name.to_s])
157
+
158
+ elsif data[plural.to_s] && meta_data && meta_data["#{name}_id"]
159
+ extract_single_relationship_from_sideload(parent, name, options, response)
160
+
161
+ else
162
+ nil # nothing found, return nil object
163
+
164
+ end
165
+ end
166
+
167
+ def extract_single_relationship_from_links(parent, name, options, url)
168
+ klass_name = options[:class_name] || ActiveSupport::Inflector.classify(name)
169
+ klass = ActiveSupport::Inflector.constantize(klass_name)
170
+ meta = parent.meta || {}
171
+ meta_data = meta[:data] || {}
172
+
173
+ store.load_single(klass, meta_data["#{name}_id"], url)
174
+ end
175
+
176
+ def extract_single_relationship_from_sideload(parent, name, options, response)
177
+ plural = ActiveSupport::Inflector.pluralize(name)
178
+ klass_name = options[:class_name] || ActiveSupport::Inflector.classify(name)
179
+ klass = ActiveSupport::Inflector.constantize(klass_name)
180
+ meta_data = parent.meta[:data]
181
+ id = meta_data["#{name}_id"]
182
+
183
+ extract_many(klass, response, plural)
184
+ .detect { |record| record.id == id }
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,93 @@
1
+ require 'json'
2
+ require 'active_support'
3
+
4
+ module RubyJsonApiClient
5
+ class JsonApiSerializer
6
+ attr_accessor :store
7
+
8
+ def transform(response)
9
+ JSON.parse(response)
10
+ end
11
+
12
+ def assert(test, failure_message)
13
+ raise failure_message if !test
14
+ end
15
+
16
+ def _json_to_model(klass, json)
17
+ json.reduce(klass.new) do |model, (field, value)|
18
+ if klass.has_field?(field.to_sym)
19
+ model.send("#{field}=", value)
20
+ end
21
+
22
+ model
23
+ end
24
+ end
25
+
26
+ def extract_single(klass, id, response)
27
+ name = klass.to_s.underscore
28
+ plural = ActiveSupport::Inflector.pluralize(name)
29
+ data = transform(response)
30
+
31
+ assert data[plural],
32
+ "No key #{plural} in json response."
33
+
34
+ assert data[plural].is_a?(Array),
35
+ "Key #{plural} should be an array"
36
+
37
+ assert data[plural][0]['id'],
38
+ "No id included in #{plural}[0] json data"
39
+
40
+ assert data[plural][0]['id'].to_s == id.to_s,
41
+ "Tried to find #{name} with id #{id}, but got #{name} with id #{data[plural][0]['id']}."
42
+
43
+ _json_to_model(klass, data[plural][0])
44
+ end
45
+
46
+ def extract_many(klass, response)
47
+ name = klass.to_s.underscore
48
+ plural = ActiveSupport::Inflector.pluralize(name)
49
+ data = transform(response)
50
+
51
+ assert data[plural],
52
+ "No key #{plural} in json response."
53
+
54
+ assert data[plural].is_a?(Array),
55
+ "Key #{plural} should be an array"
56
+
57
+ data[plural].reduce([]) do |collection, json|
58
+ collection << _json_to_model(klass, json)
59
+ end
60
+ end
61
+
62
+ def extract_many_relationship(parent, name, response)
63
+ # given response this will find the relationship
64
+ # for json api the response will either be
65
+ # 1) in links
66
+ # 2) in linked
67
+ json = transform(response)
68
+
69
+ if json['links'] && json['links'][name]
70
+ extract_many_relationship_from_links(name, json['links'][name])
71
+
72
+ elsif json['linked'] && json['linked'][name]
73
+ extract_many_relationship_from_linked(name, json['linked'][name])
74
+
75
+ else
76
+ raise "You asked for #{name} but it does not exist in links or linked"
77
+
78
+ end
79
+ end
80
+
81
+ def extract_many_relationship_from_links(name, link)
82
+ link_url = link['href']
83
+ # since we only have a url pointing to where to pull
84
+ # this info from we need to go back to the store and
85
+ # have it pull this data
86
+ store.load_collection(name, link_url)
87
+ end
88
+
89
+ def extract_relationship_from_linked(klass, name, linked)
90
+ extract_many(linked)
91
+ end
92
+ end
93
+ end