jat 0.0.1 → 0.0.3
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/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
|