jat 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +21 -0
- data/jat.gemspec +37 -0
- data/lib/jat/attribute.rb +85 -0
- data/lib/jat/config.rb +38 -0
- data/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads.rb +29 -0
- data/lib/jat/plugins/_activerecord_preloads/lib/preloader.rb +89 -0
- data/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord.rb +22 -0
- data/lib/jat/plugins/_json_api_activerecord/lib/preloads.rb +84 -0
- data/lib/jat/plugins/_preloads/_preloads.rb +53 -0
- data/lib/jat/plugins/_preloads/lib/format_user_preloads.rb +52 -0
- data/lib/jat/plugins/_preloads/lib/preloads_with_path.rb +78 -0
- data/lib/jat/plugins/cache/cache.rb +39 -0
- data/lib/jat/plugins/camel_lower/camel_lower.rb +18 -0
- data/lib/jat/plugins/json_api/json_api.rb +207 -0
- data/lib/jat/plugins/json_api/lib/construct_traversal_map.rb +91 -0
- data/lib/jat/plugins/json_api/lib/map.rb +54 -0
- data/lib/jat/plugins/json_api/lib/params/fields/parse.rb +27 -0
- data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +55 -0
- data/lib/jat/plugins/json_api/lib/params/fields.rb +23 -0
- data/lib/jat/plugins/json_api/lib/params/include/parse.rb +55 -0
- data/lib/jat/plugins/json_api/lib/params/include/validate.rb +29 -0
- data/lib/jat/plugins/json_api/lib/params/include.rb +49 -0
- data/lib/jat/plugins/json_api/lib/presenters/document_links_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/links_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/meta_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter.rb +53 -0
- data/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter.rb +53 -0
- data/lib/jat/plugins/json_api/lib/response.rb +239 -0
- data/lib/jat/plugins/json_api/lib/traversal_map.rb +34 -0
- data/lib/jat/plugins/simple_api/lib/construct_traversal_map.rb +45 -0
- data/lib/jat/plugins/simple_api/lib/map.rb +29 -0
- data/lib/jat/plugins/simple_api/lib/params/parse.rb +68 -0
- data/lib/jat/plugins/simple_api/lib/response.rb +134 -0
- data/lib/jat/plugins/simple_api/simple_api.rb +65 -0
- data/lib/jat/plugins/to_str/to_str.rb +44 -0
- data/lib/jat/plugins.rb +39 -0
- data/lib/jat/presenter.rb +51 -0
- data/lib/jat/utils/enum_deep_dup.rb +29 -0
- data/lib/jat/utils/enum_deep_freeze.rb +19 -0
- data/lib/jat.rb +66 -144
- data/test/lib/jat/attribute_test.rb +142 -0
- data/test/lib/jat/config_test.rb +57 -0
- data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +40 -0
- data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +98 -0
- data/test/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord_test.rb +29 -0
- data/test/lib/jat/plugins/_json_api_activerecord/lib/preloads_test.rb +191 -0
- data/test/lib/jat/plugins/_preloads/_preloads_test.rb +68 -0
- data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +47 -0
- data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +33 -0
- data/test/lib/jat/plugins/cache/cache_test.rb +82 -0
- data/test/lib/jat/plugins/camel_lower/camel_lower_test.rb +78 -0
- data/test/lib/jat/plugins/json_api/json_api_test.rb +154 -0
- data/test/lib/jat/plugins/json_api/lib/construct_traversal_map_test.rb +119 -0
- data/test/lib/jat/plugins/json_api/lib/map_test.rb +117 -0
- data/test/lib/jat/plugins/json_api/lib/params/fields/parse_test.rb +24 -0
- data/test/lib/jat/plugins/json_api/lib/params/fields/validate_test.rb +47 -0
- data/test/lib/jat/plugins/json_api/lib/params/fields_test.rb +37 -0
- data/test/lib/jat/plugins/json_api/lib/params/include/parse_test.rb +46 -0
- data/test/lib/jat/plugins/json_api/lib/params/include/validate_test.rb +51 -0
- data/test/lib/jat/plugins/json_api/lib/params/include_test.rb +41 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/document_links_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/links_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/meta_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter_test.rb +75 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter_test.rb +75 -0
- data/test/lib/jat/plugins/json_api/lib/response_test.rb +489 -0
- data/test/lib/jat/plugins/json_api/lib/traversal_map_test.rb +58 -0
- data/test/lib/jat/plugins/simple_api/lib/construct_traversal_map_test.rb +100 -0
- data/test/lib/jat/plugins/simple_api/lib/map_test.rb +56 -0
- data/test/lib/jat/plugins/simple_api/lib/params/parse_test.rb +71 -0
- data/test/lib/jat/plugins/simple_api/lib/response_test.rb +342 -0
- data/test/lib/jat/plugins/simple_api/simple_api_test.rb +81 -0
- data/test/lib/jat/plugins/to_str/to_str_test.rb +52 -0
- data/test/lib/jat/presenter_test.rb +61 -0
- data/test/lib/jat/utils/enum_deep_dup_test.rb +31 -0
- data/test/lib/jat/utils/enum_deep_freeze_test.rb +28 -0
- data/test/lib/jat_test.rb +120 -0
- data/test/lib/plugin_test.rb +49 -0
- data/test/support/activerecord.rb +24 -0
- data/test/test_helper.rb +16 -0
- data/test/test_plugin.rb +59 -0
- metadata +240 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3fdf9f1733a9d7ddcdeced1bcf6b6cbb1f53b9ffa11b8cb1592874dcc55c3bf2
|
4
|
+
data.tar.gz: d8982abf27a244a845ec0147a51e23946a8f6cac66c36d350aef6567253326f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f8a9be4d5e0d59922b37763de59a8833b5d3c71ac634f2c3555b6b9ab36e8a4149be04f8b33a68406616cfaeda85f49ed349fe8cc4d47bc44e0a873f0128829a
|
7
|
+
data.tar.gz: 9dcf9433b3d5f40e538db79e082095b54977f8ba8fd885ada3442441c98f374635956d0cd5201c654282954dd24d59e589a181338d2774855b97bd9677fa40d4
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# JAT (JSON API TOOLKIT)
|
2
|
+
|
3
|
+
JAT helps to serialize complex nested objects to JSON format.
|
4
|
+
|
5
|
+
Key features:
|
6
|
+
|
7
|
+
* **Auto preload** – No need to preload data manually to omit N+1 (Active Record only)
|
8
|
+
* **Configurable exposed attributes** – No more tons of serializers with different attributes sets
|
9
|
+
* **Modular design** – plugin system (aka [shrine]) allows you to load only the functionality you need
|
10
|
+
|
11
|
+
## Output Format
|
12
|
+
|
13
|
+
Supported two serialization formats:
|
14
|
+
- [JSON:API]
|
15
|
+
- Simple nested JSON objects (same as good old [AMS] or [Jbuilder])
|
16
|
+
|
17
|
+
|
18
|
+
[shrine]: https://shrinerb.com/docs/getting-started#plugin-system
|
19
|
+
[JSON:API]: https://jsonapi.org/format/
|
20
|
+
[AMS]: https://github.com/rails-api/active_model_serializers/tree/0-9-stable
|
21
|
+
[Jbuilder]: https://github.com/rails/jbuilder
|
data/jat.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "jat"
|
5
|
+
gem.version = "0.0.3"
|
6
|
+
gem.summary = "JSON API TOOLKIT"
|
7
|
+
gem.description = <<~DESC
|
8
|
+
The JAT serializer allows you to generate a JSON response based on the fields requested by the client.
|
9
|
+
Besides, it avoids manually adding includes and solves N+1 problems on its own.
|
10
|
+
DESC
|
11
|
+
|
12
|
+
gem.authors = ["Andrey Glushkov"]
|
13
|
+
gem.email = "aglushkov@shakuro.com"
|
14
|
+
gem.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "lib/**/*.rb", "jat.gemspec", "doc/**/*.md"]
|
15
|
+
gem.test_files = `git ls-files -- test/*`.split("\n")
|
16
|
+
gem.homepage = "https://github.com/aglushkov/jat"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.required_ruby_version = 2.5
|
19
|
+
gem.metadata = {
|
20
|
+
"bug_tracker_uri" => "https://github.com/aglushkov/jat/issues",
|
21
|
+
"changelog_uri" => "https://github.com/aglushkov/jat/blob/master/CHANGELOG.md",
|
22
|
+
"source_code_uri" => "https://github.com/aglushkov/jat"
|
23
|
+
}
|
24
|
+
|
25
|
+
# General testing helpers
|
26
|
+
gem.add_development_dependency "minitest", "~> 5.14"
|
27
|
+
gem.add_development_dependency "mocha", "~> 1.12"
|
28
|
+
gem.add_development_dependency "rake", "~> 13.0"
|
29
|
+
|
30
|
+
# Code standard
|
31
|
+
gem.add_development_dependency "standard", "~> 1.0"
|
32
|
+
gem.add_development_dependency "simplecov", "~> 0.21"
|
33
|
+
|
34
|
+
# ORM plugins
|
35
|
+
gem.add_development_dependency "activerecord", RUBY_VERSION >= "2.5" ? "~> 6.0" : "~> 5.2"
|
36
|
+
gem.add_development_dependency "sqlite3", "~> 1.4" unless RUBY_ENGINE == "jruby"
|
37
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "utils/enum_deep_dup"
|
4
|
+
require_relative "utils/enum_deep_freeze"
|
5
|
+
|
6
|
+
class Jat
|
7
|
+
class Attribute
|
8
|
+
module InstanceMethods
|
9
|
+
attr_reader :params, :opts
|
10
|
+
|
11
|
+
def initialize(name:, opts: {}, block: nil)
|
12
|
+
@opts = EnumDeepDup.call(opts)
|
13
|
+
@params = EnumDeepFreeze.call(name: name, opts: @opts, block: block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Attribute name that was provided when initializing attribute
|
17
|
+
def original_name
|
18
|
+
@original_name ||= params.fetch(:name).to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
# Object method name to get attribute value
|
22
|
+
def key
|
23
|
+
@key ||= opts.key?(:key) ? opts[:key].to_sym : original_name
|
24
|
+
end
|
25
|
+
|
26
|
+
# Attribute name that will be used in serialized response
|
27
|
+
def name
|
28
|
+
@name ||= original_name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Checks if attribute is exposed
|
32
|
+
def exposed?
|
33
|
+
return @exposed if instance_variable_defined?(:@exposed)
|
34
|
+
|
35
|
+
@exposed =
|
36
|
+
case self.class.jat_class.config[:exposed]
|
37
|
+
when :all then opts.fetch(:exposed, true)
|
38
|
+
when :none then opts.fetch(:exposed, false)
|
39
|
+
else opts.fetch(:exposed, !relation?)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def many?
|
44
|
+
return @many if instance_variable_defined?(:@many)
|
45
|
+
|
46
|
+
@many = opts[:many]
|
47
|
+
end
|
48
|
+
|
49
|
+
def relation?
|
50
|
+
return @relation if instance_variable_defined?(:@relation)
|
51
|
+
|
52
|
+
@relation = opts.key?(:serializer)
|
53
|
+
end
|
54
|
+
|
55
|
+
def serializer
|
56
|
+
return @serializer if instance_variable_defined?(:@serializer)
|
57
|
+
|
58
|
+
@serializer = opts[:serializer]
|
59
|
+
end
|
60
|
+
|
61
|
+
def block
|
62
|
+
@block ||=
|
63
|
+
params.fetch(:block) || begin
|
64
|
+
key_method_name = key
|
65
|
+
-> { object.public_send(key_method_name) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module ClassMethods
|
71
|
+
# Returns the Jat class that this Attribute class is namespaced under.
|
72
|
+
attr_accessor :jat_class
|
73
|
+
|
74
|
+
# Since Attribute is anonymously subclassed when Jat is subclassed,
|
75
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
76
|
+
# reflect the likely name for the class.
|
77
|
+
def inspect
|
78
|
+
"#{jat_class.inspect}::Attribute"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
extend ClassMethods
|
83
|
+
include InstanceMethods
|
84
|
+
end
|
85
|
+
end
|
data/lib/jat/config.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "utils/enum_deep_dup"
|
4
|
+
|
5
|
+
class Jat
|
6
|
+
class Config
|
7
|
+
module InstanceMethods
|
8
|
+
attr_reader :opts
|
9
|
+
|
10
|
+
def initialize(opts = {})
|
11
|
+
@opts = EnumDeepDup.call(opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(key, value)
|
15
|
+
opts[key] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](key)
|
19
|
+
opts[key]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
# Returns the Jat class that this config class is namespaced under.
|
25
|
+
attr_accessor :jat_class
|
26
|
+
|
27
|
+
# Since Config is anonymously subclassed when Jat is subclassed,
|
28
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
29
|
+
# reflect the likely name for the class.
|
30
|
+
def inspect
|
31
|
+
"#{jat_class.inspect}::Config"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
include InstanceMethods
|
36
|
+
extend ClassMethods
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./lib/preloader"
|
4
|
+
|
5
|
+
class Jat
|
6
|
+
module Plugins
|
7
|
+
module ActiverecordPreloads
|
8
|
+
module InstanceMethods
|
9
|
+
def initialize(*)
|
10
|
+
super
|
11
|
+
@object = add_preloads(@object)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def add_preloads(obj)
|
17
|
+
return obj if obj.nil? || (obj.is_a?(Array) && obj.empty?)
|
18
|
+
|
19
|
+
preloads = self.class.jat_preloads(self)
|
20
|
+
return obj if preloads.empty?
|
21
|
+
|
22
|
+
Preloader.preload(obj, preloads)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
register_plugin(:_activerecord_preloads, ActiverecordPreloads)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module ActiverecordPreloads
|
6
|
+
class Preloader
|
7
|
+
module ClassMethods
|
8
|
+
def preload(object, preloads)
|
9
|
+
preload_handler = handlers.find { |handler| handler.fit?(object) }
|
10
|
+
raise Error, "Can't preload #{preloads.inspect} to #{object.inspect}" unless preload_handler
|
11
|
+
|
12
|
+
preload_handler.preload(object, preloads)
|
13
|
+
end
|
14
|
+
|
15
|
+
def handlers
|
16
|
+
@handlers ||= [ActiverecordRelation, ActiverecordObject, ActiverecordArray].freeze
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
extend ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
class ActiverecordObject
|
24
|
+
module ClassMethods
|
25
|
+
def fit?(object)
|
26
|
+
object.is_a?(ActiveRecord::Base)
|
27
|
+
end
|
28
|
+
|
29
|
+
def preload(object, preloads)
|
30
|
+
# Reset associations that will be preloaded to fix possible bugs with
|
31
|
+
# ActiveRecord::Associations::Preloader
|
32
|
+
preloads.each_key { |key| object.association(key).reset }
|
33
|
+
ActiveRecord::Associations::Preloader.new.preload(object, preloads)
|
34
|
+
|
35
|
+
object
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
extend ClassMethods
|
40
|
+
end
|
41
|
+
|
42
|
+
class ActiverecordRelation
|
43
|
+
module ClassMethods
|
44
|
+
def fit?(objects)
|
45
|
+
objects.is_a?(ActiveRecord::Relation)
|
46
|
+
end
|
47
|
+
|
48
|
+
def preload(objects, preloads)
|
49
|
+
objects.preload(preloads)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
extend ClassMethods
|
54
|
+
end
|
55
|
+
|
56
|
+
class ActiverecordArray
|
57
|
+
module ClassMethods
|
58
|
+
def fit?(objects)
|
59
|
+
objects.is_a?(Array) &&
|
60
|
+
ActiverecordObject.fit?(objects.first) &&
|
61
|
+
same_kind?(objects)
|
62
|
+
end
|
63
|
+
|
64
|
+
def preload(objects, preloads)
|
65
|
+
# Reset associations that will be preloaded to fix possible bugs with
|
66
|
+
# ActiveRecord::Associations::Preloader
|
67
|
+
preloads.each_key { |key| reset_association(objects, key) }
|
68
|
+
ActiveRecord::Associations::Preloader.new.preload(objects, preloads)
|
69
|
+
|
70
|
+
objects
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def reset_association(objects, key)
|
76
|
+
objects.each { |object| object.association(key).reset }
|
77
|
+
end
|
78
|
+
|
79
|
+
def same_kind?(objects)
|
80
|
+
first_object_class = objects.first.class
|
81
|
+
objects.all? { |object| object.instance_of?(first_object_class) }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
extend ClassMethods
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./lib/preloads"
|
4
|
+
|
5
|
+
class Jat
|
6
|
+
module Plugins
|
7
|
+
module JsonApiActiverecord
|
8
|
+
def self.after_load(jat_class, **opts)
|
9
|
+
jat_class.plugin :_preloads, **opts
|
10
|
+
jat_class.plugin :_activerecord_preloads, **opts
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def jat_preloads(jat)
|
15
|
+
Preloads.call(jat)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
register_plugin(:_json_api_activerecord, JsonApiActiverecord)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module JsonApiActiverecord
|
6
|
+
class Preloads
|
7
|
+
class << self
|
8
|
+
def call(jat)
|
9
|
+
new(jat.traversal_map.current).for(jat.class)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :initial_result
|
14
|
+
|
15
|
+
def initialize(current_map)
|
16
|
+
@current_map = current_map
|
17
|
+
@initial_result = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def for(jat_class)
|
21
|
+
@initial_result = {}
|
22
|
+
append(initial_result, jat_class)
|
23
|
+
initial_result
|
24
|
+
rescue SystemStackError
|
25
|
+
raise Error, "Stack level too deep, recursive preloads detected: #{initial_result}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :current_map
|
31
|
+
|
32
|
+
def append(result, jat_class)
|
33
|
+
attrs = current_map[jat_class.type]
|
34
|
+
attributes_names = attrs[:attributes] + attrs[:relationships]
|
35
|
+
|
36
|
+
add_attributes(result, jat_class, attributes_names)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_attributes(result, jat_class, attributes_names)
|
40
|
+
attributes_names.each do |name|
|
41
|
+
attribute = jat_class.attributes[name]
|
42
|
+
preloads = attribute.preloads
|
43
|
+
next unless preloads # we should not addd preloads and nested preloads when nil provided
|
44
|
+
|
45
|
+
add_preloads(result, preloads, attribute)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_preloads(result, preloads, attribute)
|
50
|
+
unless preloads.empty?
|
51
|
+
preloads = deep_dup(preloads)
|
52
|
+
merge(result, preloads)
|
53
|
+
end
|
54
|
+
|
55
|
+
add_nested_preloads(result, attribute) if attribute.relation?
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_nested_preloads(result, attribute)
|
59
|
+
path = attribute.preloads_path
|
60
|
+
nested_result = nested(result, path)
|
61
|
+
nested_serializer = attribute.serializer.call
|
62
|
+
|
63
|
+
append(nested_result, nested_serializer)
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge(result, preloads)
|
67
|
+
result.merge!(preloads) do |_key, value_one, value_two|
|
68
|
+
merge(value_one, value_two)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def deep_dup(preloads)
|
73
|
+
preloads.dup.transform_values! do |nested_preloads|
|
74
|
+
deep_dup(nested_preloads)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def nested(result, path)
|
79
|
+
!path || path.empty? ? result : result.dig(*path)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./lib/format_user_preloads"
|
4
|
+
require_relative "./lib/preloads_with_path"
|
5
|
+
|
6
|
+
# This plugin adds attribute methods #preloads, #preloads_path
|
7
|
+
class Jat
|
8
|
+
module Plugins
|
9
|
+
module Preloads
|
10
|
+
module AttributeMethods
|
11
|
+
NULL_PRELOADS = [nil, [].freeze].freeze
|
12
|
+
|
13
|
+
def preloads
|
14
|
+
return @preloads if defined?(@preloads)
|
15
|
+
|
16
|
+
@preloads, @preloads_path = get_preloads_with_path
|
17
|
+
@preloads
|
18
|
+
end
|
19
|
+
|
20
|
+
def preloads_path
|
21
|
+
return @preloads_path if defined?(@preloads_path)
|
22
|
+
|
23
|
+
@preloads, @preloads_path = get_preloads_with_path
|
24
|
+
@preloads_path
|
25
|
+
end
|
26
|
+
|
27
|
+
# When provided multiple values in preloads, such as { user: [:profile] },
|
28
|
+
# we don't know which entity is main (:user or :profile in this example) but
|
29
|
+
# we need to know main value to add nested preloads to it.
|
30
|
+
# User can specify main preloaded entity by adding "!" suffix
|
31
|
+
# ({ user!: [:profile] } for example), othervice the latest key will be considered main.
|
32
|
+
def get_preloads_with_path
|
33
|
+
preloads_provided = opts.key?(:preload)
|
34
|
+
preloads =
|
35
|
+
if preloads_provided
|
36
|
+
opts[:preload]
|
37
|
+
elsif relation?
|
38
|
+
key
|
39
|
+
end
|
40
|
+
|
41
|
+
# Nulls and empty hash differs as we can preload nested results to
|
42
|
+
# empty hash, but we will skip nested preloading if null or false provided
|
43
|
+
return NULL_PRELOADS if preloads_provided && !preloads
|
44
|
+
|
45
|
+
preloads = FormatUserPreloads.to_hash(preloads)
|
46
|
+
PreloadsWithPath.call(preloads)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
register_plugin(:_preloads, Preloads)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
class FormatUserPreloads
|
7
|
+
METHODS = {
|
8
|
+
Array => :array_to_hash,
|
9
|
+
FalseClass => :nil_to_hash,
|
10
|
+
Hash => :hash_to_hash,
|
11
|
+
NilClass => :nil_to_hash,
|
12
|
+
String => :string_to_hash,
|
13
|
+
Symbol => :symbol_to_hash
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def to_hash(value)
|
18
|
+
send(METHODS.fetch(value.class), value)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def array_to_hash(values)
|
24
|
+
values.each_with_object({}) do |value, obj|
|
25
|
+
obj.merge!(to_hash(value))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def hash_to_hash(values)
|
30
|
+
values.each_with_object({}) do |(key, value), obj|
|
31
|
+
obj[key.to_sym] = to_hash(value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def nil_to_hash(_value)
|
36
|
+
{}
|
37
|
+
end
|
38
|
+
|
39
|
+
def string_to_hash(value)
|
40
|
+
symbol_to_hash(value.to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
def symbol_to_hash(value)
|
44
|
+
{value => {}}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
extend ClassMethods
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
class PreloadsWithPath
|
7
|
+
module ClassMethods
|
8
|
+
BANG = "!"
|
9
|
+
NO_PRELOADS = [{}.freeze, [].freeze].freeze
|
10
|
+
|
11
|
+
# @param preload<Hash> Formatted user provided preloads hash
|
12
|
+
def call(preloads)
|
13
|
+
return NO_PRELOADS if preloads.empty?
|
14
|
+
|
15
|
+
path = main_path(preloads)
|
16
|
+
return [preloads, path] unless has_bang?(path)
|
17
|
+
|
18
|
+
# We should remove bangs from last key in path and from associated preloads key.
|
19
|
+
# We use mutable methods here.
|
20
|
+
remove_bangs(preloads, path)
|
21
|
+
[preloads, path]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Generates path (Array) to main included resource.
|
27
|
+
# We need to know main included resource to include nested associations.
|
28
|
+
#
|
29
|
+
# User should mark main included resource with "!"
|
30
|
+
# When nothing marked, last included resource is considered main.
|
31
|
+
#
|
32
|
+
# main_path(a: { b!: { c: {} }, d: {} }) # => [:a, :b]
|
33
|
+
# main_path(a: { b: { c: {} }, d: {} }) # => [:a, :d]
|
34
|
+
#
|
35
|
+
def main_path(hash, path = [])
|
36
|
+
current_level = path.size
|
37
|
+
|
38
|
+
hash.each do |key, data|
|
39
|
+
path.pop(path.size - current_level)
|
40
|
+
path << key
|
41
|
+
return path if key[-1] == BANG
|
42
|
+
|
43
|
+
main_path(data, path)
|
44
|
+
return path if path.last[-1] == BANG
|
45
|
+
end
|
46
|
+
|
47
|
+
path
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_bangs(preloads, path)
|
51
|
+
# Remove last path with bang
|
52
|
+
bang_key = path.pop
|
53
|
+
|
54
|
+
# Delete bang from key
|
55
|
+
key = bang_key.to_s.delete_suffix!(BANG).to_sym
|
56
|
+
|
57
|
+
# Navigate to main resource and replace key with BANG
|
58
|
+
nested_preloads = empty_dig(preloads, path)
|
59
|
+
nested_preloads[key] = nested_preloads.delete(bang_key)
|
60
|
+
|
61
|
+
# Add cleared key to path
|
62
|
+
path << key
|
63
|
+
end
|
64
|
+
|
65
|
+
def empty_dig(hash, path)
|
66
|
+
path.empty? ? hash : hash.dig(*path)
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_bang?(path)
|
70
|
+
path.last[-1] == BANG
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
extend ClassMethods
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module Cache
|
6
|
+
def self.before_load(jat_class, **opts)
|
7
|
+
jat_class.plugin :to_str, **opts
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
FORMAT_TO_STR = :to_str
|
12
|
+
FORMAT_TO_H = :to_h
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
return super if context[:_format] == FORMAT_TO_STR
|
16
|
+
|
17
|
+
context[:_format] = FORMAT_TO_H
|
18
|
+
cached { super }
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_str
|
22
|
+
context[:_format] = FORMAT_TO_STR
|
23
|
+
cached { super }
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def cached(&block)
|
29
|
+
cache = context[:cache]
|
30
|
+
return yield unless cache
|
31
|
+
|
32
|
+
cache.call(object, context, &block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
register_plugin(:cache, Cache)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module CamelLower
|
6
|
+
module AttributeMethods
|
7
|
+
def name
|
8
|
+
first_word, *other = original_name.to_s.split("_")
|
9
|
+
last_words = other.map!(&:capitalize).join
|
10
|
+
|
11
|
+
:"#{first_word}#{last_words}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
register_plugin(:camel_lower, CamelLower)
|
17
|
+
end
|
18
|
+
end
|