json_api_model 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +6 -5
- data/README.md +59 -0
- data/json_api_model.gemspec +1 -0
- data/lib/json_api_model.rb +2 -0
- data/lib/json_api_model/associatable.rb +70 -0
- data/lib/json_api_model/associations.rb +10 -0
- data/lib/json_api_model/associations/base.rb +65 -0
- data/lib/json_api_model/associations/belongs_to.rb +23 -0
- data/lib/json_api_model/associations/flattable.rb +25 -0
- data/lib/json_api_model/associations/has.rb +46 -0
- data/lib/json_api_model/associations/has_many.rb +6 -0
- data/lib/json_api_model/associations/has_one.rb +7 -0
- data/lib/json_api_model/model.rb +6 -9
- data/lib/json_api_model/scope.rb +49 -11
- data/lib/json_api_model/version.rb +1 -1
- metadata +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4309beecbe58dd95fe10e0aa45cfe3c9ab93710
|
4
|
+
data.tar.gz: 2cc02483222c4319429bd5539879cd8c20fe00d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 137fd5629ae09611ef523a7092d4fe552900d78f189de0cc74d097f3b517144f81a556fbd54aa0d1768a7987617617a5751ce8baee4d8b1fcc18db5b77795502
|
7
|
+
data.tar.gz: f69d1952b0a22558d269e8549b40e1ed451e3df3c1c589541652cecb75c71c7ab45ecdff9eea550a53f3d4fd7ca2a5362462af2eeb17eb9395c0d286f2bd99be
|
data/Gemfile.lock
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
json_api_model (0.
|
4
|
+
json_api_model (0.3.0)
|
5
|
+
activesupport
|
5
6
|
json_api_client
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
9
10
|
specs:
|
10
|
-
activemodel (5.2.
|
11
|
-
activesupport (= 5.2.
|
12
|
-
activesupport (5.2.
|
11
|
+
activemodel (5.2.1)
|
12
|
+
activesupport (= 5.2.1)
|
13
|
+
activesupport (5.2.1)
|
13
14
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
15
|
i18n (>= 0.7, < 2)
|
15
16
|
minitest (~> 5.1)
|
@@ -25,7 +26,7 @@ GEM
|
|
25
26
|
faraday_middleware (0.12.2)
|
26
27
|
faraday (>= 0.7.4, < 1.0)
|
27
28
|
hashdiff (0.3.7)
|
28
|
-
i18n (1.0
|
29
|
+
i18n (1.1.0)
|
29
30
|
concurrent-ruby (~> 1.0)
|
30
31
|
json (2.1.0)
|
31
32
|
json_api_client (1.5.3)
|
data/README.md
CHANGED
@@ -31,6 +31,61 @@ Using the model wrappers is pretty straighforward. It thinly wraps `JsonApiClien
|
|
31
31
|
|
32
32
|
Any instance or class(non-query) level method will fall thorugh to the client.
|
33
33
|
|
34
|
+
### Associations
|
35
|
+
|
36
|
+
You can define simple associations that behave very much like ActiveRecord associations. Once you define your association, you will have a method with that name that will do the lookups and cache the results for you.
|
37
|
+
|
38
|
+
* `belongs_to association`: the base object has to have the association id
|
39
|
+
* will return a single object or nil
|
40
|
+
* `has_one`: the assiociation object has the id of the base.
|
41
|
+
* will return a single object or nil
|
42
|
+
* `has_many`: the association object has the id of the base.
|
43
|
+
* will return an array
|
44
|
+
|
45
|
+
#### Assocaition Options
|
46
|
+
|
47
|
+
Associations have some of the standard ActiveRecord options. Namely:
|
48
|
+
* `class`: specifies the class to find the record in.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
has_one :special_thing, class: Thing
|
52
|
+
```
|
53
|
+
|
54
|
+
* `class_name`: specifies the class w.o having to have the class defined. Handy for circular dependencies
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class Person < JsonApiModel::Model
|
58
|
+
wraps MyApi::Client::Person
|
59
|
+
has_one :nickname, class_name: "Pesudonym"
|
60
|
+
end
|
61
|
+
|
62
|
+
class Pseudonym < JsonApiModel::Model
|
63
|
+
wraps MyApi::Client::Pseudonym
|
64
|
+
belongs_to :bearer, class_name: "Person"
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
* `through`: many to may association helper.
|
69
|
+
|
70
|
+
__NOTE__: Due to perf implications this is __only__ available for local classes (not things that come through the `relatinships` block in the payload). This will __*never ever*__ be available for remote models.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class Person < JsonApiModel::Model
|
74
|
+
wraps MyApi::Client::Person
|
75
|
+
has_many :person_foods
|
76
|
+
has_many :favorite_foods, through: :person_foods, class_name: "Food"
|
77
|
+
end
|
78
|
+
|
79
|
+
# locally stored models that respond to #{model_class}_id
|
80
|
+
class PersonFood < ActiveRecord::Base
|
81
|
+
belongs_to :person
|
82
|
+
belongs_to :food
|
83
|
+
end
|
84
|
+
|
85
|
+
class Food < ActiveRecord::Base
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
34
89
|
### Example
|
35
90
|
|
36
91
|
If you have an app that talks to a `user` service. Here's how your `User` model might look:
|
@@ -173,6 +228,10 @@ ActiveSupport::Notifications.subscribe "find.json_api_model" do |name, started,
|
|
173
228
|
end
|
174
229
|
```
|
175
230
|
|
231
|
+
## Known Issues
|
232
|
+
|
233
|
+
* Due to an open issue in `JsonApiClient` scopes are modifiable, which means that `scope.where( params )` now permanently has those params in it.
|
234
|
+
|
176
235
|
## Development
|
177
236
|
|
178
237
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/json_api_model.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
+
spec.add_dependency "activesupport"
|
23
24
|
spec.add_dependency "json_api_client"
|
24
25
|
|
25
26
|
spec.add_development_dependency "bundler", "~> 1.16"
|
data/lib/json_api_model.rb
CHANGED
@@ -2,6 +2,8 @@ require "json_api_model/version"
|
|
2
2
|
|
3
3
|
module JsonApiModel
|
4
4
|
|
5
|
+
autoload :Associations, 'json_api_model/associations'
|
6
|
+
autoload :Associatable, 'json_api_model/associatable'
|
5
7
|
autoload :Model, 'json_api_model/model'
|
6
8
|
autoload :Instrumenter, 'json_api_model/instrumenter'
|
7
9
|
autoload :ResultSet, 'json_api_model/result_set'
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associatable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
|
7
|
+
class_attribute :__associations
|
8
|
+
self.__associations = {}
|
9
|
+
|
10
|
+
attr_accessor :__cached_associations
|
11
|
+
|
12
|
+
def has_relationship_ids?( name )
|
13
|
+
!!relationships[ name ]
|
14
|
+
end
|
15
|
+
|
16
|
+
def relationship_ids( name )
|
17
|
+
relationships_data = relationships[ name ]&.dig( :data )
|
18
|
+
case relationships_data
|
19
|
+
when Hash
|
20
|
+
[ relationships_data[ :id ] ]
|
21
|
+
when Array
|
22
|
+
relationships_data.map{ | datum | datum[ :id ] }
|
23
|
+
when NilClass
|
24
|
+
[ ]
|
25
|
+
else
|
26
|
+
raise "Unexpected relationship data type: #{relationships_data.class}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
|
32
|
+
def belongs_to( name, opts = {} )
|
33
|
+
process Associations::BelongsTo.new( self, name, opts )
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_one( name, opts = {} )
|
37
|
+
process Associations::HasOne.new( self, name, opts )
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_many( name, opts = {} )
|
41
|
+
process Associations::HasMany.new( self, name, opts )
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def process( association )
|
47
|
+
associate association
|
48
|
+
methodize association
|
49
|
+
end
|
50
|
+
|
51
|
+
def associate( association )
|
52
|
+
self.__associations = __associations.merge association.name => association
|
53
|
+
end
|
54
|
+
|
55
|
+
def methodize( association )
|
56
|
+
define_method association.name do
|
57
|
+
self.__cached_associations ||= {}
|
58
|
+
|
59
|
+
unless self.__cached_associations.has_key? association.name
|
60
|
+
result = association.fetch( self )
|
61
|
+
|
62
|
+
self.__cached_associations[association.name] = result
|
63
|
+
end
|
64
|
+
self.__cached_associations[association.name]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
autoload :Base, 'json_api_model/associations/base'
|
4
|
+
autoload :BelongsTo, 'json_api_model/associations/belongs_to'
|
5
|
+
autoload :Flattable, 'json_api_model/associations/flattable'
|
6
|
+
autoload :Has, 'json_api_model/associations/has'
|
7
|
+
autoload :HasMany, 'json_api_model/associations/has_many'
|
8
|
+
autoload :HasOne, 'json_api_model/associations/has_one'
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
class Base
|
4
|
+
|
5
|
+
attr_accessor :name, :opts, :key
|
6
|
+
|
7
|
+
def initialize( base_class, name, opts = {} )
|
8
|
+
self.name = name
|
9
|
+
self.opts = opts
|
10
|
+
self.key = idify( base_class )
|
11
|
+
|
12
|
+
sanitize_opts( base_class )
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch( instance )
|
16
|
+
process klass.send( action, query( instance ) )
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def klass
|
22
|
+
@klass ||= association_class( name, opts )
|
23
|
+
end
|
24
|
+
|
25
|
+
def association_class( name, opts = {} )
|
26
|
+
a_class = opts[:class] if opts.has_key? :class
|
27
|
+
a_class_name = opts[:class_name].constantize if opts.has_key? :class_name
|
28
|
+
|
29
|
+
a_class || a_class_name || derived_class_for( name )
|
30
|
+
end
|
31
|
+
|
32
|
+
def derived_class_for( name )
|
33
|
+
name.to_s.singularize.classify.constantize
|
34
|
+
end
|
35
|
+
|
36
|
+
def idify( class_name )
|
37
|
+
"#{class_name.to_s.demodulize.underscore}_id"
|
38
|
+
end
|
39
|
+
|
40
|
+
def supported_options
|
41
|
+
[ :class, :class_name ] + additional_options
|
42
|
+
end
|
43
|
+
|
44
|
+
def additional_options
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
|
48
|
+
def sanitize_opts( base_class )
|
49
|
+
if name.to_s == "object"
|
50
|
+
raise "#{base_class}: 'object_id' is a reserved keyword in ruby and cannot be overridden"
|
51
|
+
end
|
52
|
+
invalid_options = (opts.keys - supported_options)
|
53
|
+
if invalid_options.present?
|
54
|
+
list = invalid_options.map{|o|"'#{o}'"}.to_sentence
|
55
|
+
plural = invalid_options.count > 1
|
56
|
+
raise "#{base_class}: #{list} #{plural ? "are" : "is"} not supported."
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def process( results )
|
61
|
+
results
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
class BelongsTo < Base
|
4
|
+
include Flattable
|
5
|
+
|
6
|
+
def action
|
7
|
+
:find
|
8
|
+
end
|
9
|
+
|
10
|
+
def key
|
11
|
+
"#{name}_id"
|
12
|
+
end
|
13
|
+
|
14
|
+
def query( instance )
|
15
|
+
if instance.has_relationship_ids? name
|
16
|
+
{ id: instance.relationship_ids( name ) }
|
17
|
+
else
|
18
|
+
instance.send key
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
module Flattable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
def process( results )
|
8
|
+
if flattable? results
|
9
|
+
results.first
|
10
|
+
else
|
11
|
+
results
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def flattable?( results )
|
18
|
+
results.is_a?( JsonApiModel::ResultSet ) ||
|
19
|
+
results.is_a?( Array ) ||
|
20
|
+
results.is_a?( JsonApiModel::Scope )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
class Has < Base
|
4
|
+
|
5
|
+
def action
|
6
|
+
:where
|
7
|
+
end
|
8
|
+
|
9
|
+
def query( instance )
|
10
|
+
if instance.has_relationship_ids? name
|
11
|
+
{ id: instance.relationship_ids( name ) }
|
12
|
+
elsif through?
|
13
|
+
{ id: target_ids( instance ) }
|
14
|
+
else
|
15
|
+
{ key => instance.id }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def additional_options
|
20
|
+
[ :through ]
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def through
|
26
|
+
opts[:through]
|
27
|
+
end
|
28
|
+
|
29
|
+
def through?
|
30
|
+
opts.has_key? :through
|
31
|
+
end
|
32
|
+
|
33
|
+
def target_ids( instance )
|
34
|
+
intermadiates = Array(instance.send( through ) )
|
35
|
+
|
36
|
+
intermadiates.map do | intermediate |
|
37
|
+
intermediate.send( through_key )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def through_key
|
42
|
+
idify klass
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/json_api_model/model.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
module JsonApiModel
|
2
2
|
class Model
|
3
3
|
class << self
|
4
|
-
|
5
|
-
|
6
|
-
def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
|
4
|
+
delegate :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last, to: :__new_scope
|
7
5
|
|
8
6
|
attr_reader :client_class
|
9
7
|
def wraps( client_class )
|
@@ -36,13 +34,16 @@ module JsonApiModel
|
|
36
34
|
|
37
35
|
private
|
38
36
|
|
39
|
-
def
|
37
|
+
def __new_scope
|
40
38
|
Scope.new( self )
|
41
39
|
end
|
42
40
|
end
|
43
|
-
|
41
|
+
include Associatable
|
42
|
+
|
44
43
|
attr_accessor :client
|
45
44
|
|
45
|
+
delegate :as_json, to: :client
|
46
|
+
|
46
47
|
def initialize( attributes = {} )
|
47
48
|
@client = self.class.client_class.new( attributes )
|
48
49
|
end
|
@@ -53,10 +54,6 @@ module JsonApiModel
|
|
53
54
|
raise NoMethodError, "No method `#{m}' found in #{self} or #{client}"
|
54
55
|
end
|
55
56
|
|
56
|
-
def as_json
|
57
|
-
client.as_json
|
58
|
-
end
|
59
|
-
|
60
57
|
RESERVED_FIELDS = [ :type, :id ]
|
61
58
|
end
|
62
59
|
end
|
data/lib/json_api_model/scope.rb
CHANGED
@@ -3,33 +3,42 @@ module JsonApiModel
|
|
3
3
|
def initialize( model_class )
|
4
4
|
@model_class = model_class
|
5
5
|
@client_scope = JsonApiClient::Query::Builder.new( model_class.client_class )
|
6
|
+
@cache = {}
|
6
7
|
end
|
7
8
|
|
8
|
-
def to_a
|
9
|
-
@to_a ||= find
|
10
|
-
end
|
11
|
-
|
12
|
-
alias all to_a
|
13
|
-
|
14
|
-
|
15
9
|
def find( args = {} )
|
16
10
|
JsonApiModel.instrumenter.instrument 'find.json_api_model',
|
17
11
|
args: args,
|
18
12
|
url: url do
|
19
|
-
|
20
|
-
|
13
|
+
cache_or_find args do
|
14
|
+
results = @client_scope.find args
|
15
|
+
ResultSet.new( results, @model_class )
|
16
|
+
end
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
20
|
+
alias to_a find
|
21
|
+
alias all find
|
22
|
+
|
24
23
|
def first
|
25
24
|
JsonApiModel.instrumenter.instrument 'first.json_api_model', url: url do
|
26
|
-
|
25
|
+
# if the non-first query has already been executed, there's no need to make the call again
|
26
|
+
if cached?
|
27
|
+
cache.first
|
28
|
+
else
|
29
|
+
cache_or_find :first do
|
30
|
+
@model_class.new_from_client @client_scope.first
|
31
|
+
end
|
32
|
+
end
|
27
33
|
end
|
28
34
|
end
|
29
35
|
|
30
36
|
def last
|
31
37
|
JsonApiModel.instrumenter.instrument 'last.json_api_model', url: url do
|
32
|
-
|
38
|
+
# this is a separate call always because the last record may exceed page size
|
39
|
+
cache_or_find :last do
|
40
|
+
@model_class.new_from_client @client_scope.last
|
41
|
+
end
|
33
42
|
end
|
34
43
|
end
|
35
44
|
|
@@ -48,6 +57,35 @@ module JsonApiModel
|
|
48
57
|
|
49
58
|
private
|
50
59
|
|
60
|
+
def cached?
|
61
|
+
@cache.has_key? keyify
|
62
|
+
end
|
63
|
+
|
64
|
+
def cache
|
65
|
+
@cache[keyify]
|
66
|
+
end
|
67
|
+
|
68
|
+
# because a scope can be modified and then resolved, we want to cache by the full param set
|
69
|
+
def cache_or_find( opts = {} )
|
70
|
+
key = keyify( opts )
|
71
|
+
@cache.fetch key do |key|
|
72
|
+
@cache[key] = yield
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def keyify( opts = {} )
|
77
|
+
params.merge( hashify( opts ) ).sort.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def hashify( opts = {} )
|
81
|
+
case opts
|
82
|
+
when Hash
|
83
|
+
opts
|
84
|
+
else
|
85
|
+
{ opt: opts }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
51
89
|
def url
|
52
90
|
@model_class.client_class.requestor.send( :resource_path, params )
|
53
91
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_api_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Orlov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-08-
|
11
|
+
date: 2018-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: json_api_client
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -113,6 +127,14 @@ files:
|
|
113
127
|
- bin/setup
|
114
128
|
- json_api_model.gemspec
|
115
129
|
- lib/json_api_model.rb
|
130
|
+
- lib/json_api_model/associatable.rb
|
131
|
+
- lib/json_api_model/associations.rb
|
132
|
+
- lib/json_api_model/associations/base.rb
|
133
|
+
- lib/json_api_model/associations/belongs_to.rb
|
134
|
+
- lib/json_api_model/associations/flattable.rb
|
135
|
+
- lib/json_api_model/associations/has.rb
|
136
|
+
- lib/json_api_model/associations/has_many.rb
|
137
|
+
- lib/json_api_model/associations/has_one.rb
|
116
138
|
- lib/json_api_model/instrumenter.rb
|
117
139
|
- lib/json_api_model/instrumenter/null_instrumenter.rb
|
118
140
|
- lib/json_api_model/model.rb
|