jat 0.0.1 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jat/attribute.rb +107 -0
- data/lib/jat/config.rb +35 -0
- data/lib/jat/plugins/activerecord/activerecord.rb +23 -0
- data/lib/jat/plugins/cache/cache.rb +47 -0
- data/lib/jat/plugins/common/_activerecord_preloads/_activerecord_preloads.rb +38 -0
- data/lib/jat/plugins/common/_activerecord_preloads/lib/preloader.rb +93 -0
- data/lib/jat/plugins/common/_lower_camel_case/_lower_camel_case.rb +36 -0
- data/lib/jat/plugins/common/_preloads/_preloads.rb +63 -0
- data/lib/jat/plugins/common/_preloads/lib/format_user_preloads.rb +52 -0
- data/lib/jat/plugins/common/_preloads/lib/preloads_with_path.rb +78 -0
- data/lib/jat/plugins/json_api/json_api.rb +251 -0
- data/lib/jat/plugins/json_api/lib/fields_param_parser.rb +40 -0
- data/lib/jat/plugins/json_api/lib/include_param_parser.rb +84 -0
- data/lib/jat/plugins/json_api/lib/map.rb +119 -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 +58 -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/response.rb +123 -0
- data/lib/jat/plugins/json_api/lib/response_piece.rb +175 -0
- data/lib/jat/plugins/json_api/plugins/json_api_activerecord/json_api_activerecord.rb +23 -0
- data/lib/jat/plugins/json_api/plugins/json_api_lower_camel_case/json_api_lower_camel_case.rb +34 -0
- data/lib/jat/plugins/json_api/plugins/json_api_maps_cache/json_api_maps_cache.rb +58 -0
- data/lib/jat/plugins/json_api/plugins/json_api_preloads/json_api_preloads.rb +38 -0
- data/lib/jat/plugins/json_api/plugins/json_api_preloads/lib/preloads.rb +76 -0
- data/lib/jat/plugins/json_api/plugins/json_api_validate_params/json_api_validate_params.rb +61 -0
- data/lib/jat/plugins/json_api/plugins/json_api_validate_params/lib/params_error.rb +6 -0
- data/lib/jat/plugins/json_api/plugins/json_api_validate_params/lib/validate_fields_param.rb +59 -0
- data/lib/jat/plugins/json_api/plugins/json_api_validate_params/lib/validate_include_param.rb +33 -0
- data/lib/jat/plugins/lower_camel_case/lower_camel_case.rb +23 -0
- data/lib/jat/plugins/maps_cache/maps_cache.rb +23 -0
- data/lib/jat/plugins/preloads/preloads.rb +23 -0
- data/lib/jat/plugins/simple_api/lib/fields_param_parser.rb +97 -0
- data/lib/jat/plugins/simple_api/lib/map.rb +99 -0
- data/lib/jat/plugins/simple_api/lib/response.rb +119 -0
- data/lib/jat/plugins/simple_api/lib/response_piece.rb +80 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_activerecord/simple_api_activerecord.rb +23 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_lower_camel_case/simple_api_lower_camel_case.rb +34 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_maps_cache/simple_api_maps_cache.rb +50 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_preloads/lib/preloads.rb +55 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_preloads/simple_api_preloads.rb +38 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_validate_params/lib/fields_error.rb +6 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_validate_params/lib/validate_fields_param.rb +45 -0
- data/lib/jat/plugins/simple_api/plugins/simple_api_validate_params/simple_api_validate_params.rb +49 -0
- data/lib/jat/plugins/simple_api/simple_api.rb +125 -0
- data/lib/jat/plugins/to_str/to_str.rb +54 -0
- data/lib/jat/plugins/types/types.rb +54 -0
- data/lib/jat/plugins/validate_params/validate_params.rb +23 -0
- data/lib/jat/plugins.rb +39 -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 -141
- data/test/lib/jat/attribute_test.rb +152 -0
- data/test/lib/jat/config_test.rb +57 -0
- data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +59 -0
- data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +84 -0
- data/test/lib/jat/plugins/_camel_lower/_camel_lower_test.rb +26 -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/json_api/json_api_test.rb +162 -0
- data/test/lib/jat/plugins/json_api/lib/fields_param_parser_test.rb +38 -0
- data/test/lib/jat/plugins/json_api/lib/include_param_parser_test.rb +41 -0
- data/test/lib/jat/plugins/json_api/lib/map_test.rb +188 -0
- data/test/lib/jat/plugins/json_api/lib/response_test.rb +489 -0
- data/test/lib/jat/plugins/json_api_activerecord/json_api_activerecord_test.rb +24 -0
- data/test/lib/jat/plugins/json_api_camel_lower/json_api_camel_lower_test.rb +79 -0
- data/test/lib/jat/plugins/json_api_maps_cache/json_api_maps_cache_test.rb +107 -0
- data/test/lib/jat/plugins/json_api_preloads/json_api_preloads_test.rb +37 -0
- data/test/lib/jat/plugins/json_api_preloads/lib/preloads_test.rb +197 -0
- data/test/lib/jat/plugins/json_api_validate_params/json_api_validate_params_test.rb +84 -0
- data/test/lib/jat/plugins/simple_api/lib/fields_param_parser_test.rb +77 -0
- data/test/lib/jat/plugins/simple_api/lib/map_test.rb +133 -0
- data/test/lib/jat/plugins/simple_api/lib/response_test.rb +348 -0
- data/test/lib/jat/plugins/simple_api/simple_api_test.rb +139 -0
- data/test/lib/jat/plugins/simple_api_activerecord/simple_api_activerecord_test.rb +24 -0
- data/test/lib/jat/plugins/simple_api_camel_lower/simple_api_camel_lower_test.rb +48 -0
- data/test/lib/jat/plugins/simple_api_maps_cache/simple_api_maps_cache_test.rb +95 -0
- data/test/lib/jat/plugins/simple_api_preloads/lib/preloads_test.rb +140 -0
- data/test/lib/jat/plugins/simple_api_preloads/simple_api_preloads_test.rb +37 -0
- data/test/lib/jat/plugins/simple_api_validate_params/simple_api_validate_params_test.rb +89 -0
- data/test/lib/jat/plugins/to_str/to_str_test.rb +52 -0
- data/test/lib/jat/plugins/types/types_test.rb +79 -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 +143 -0
- data/test/lib/plugin_test.rb +49 -0
- data/test/support/activerecord.rb +24 -0
- data/test/test_helper.rb +13 -0
- data/test/test_plugin.rb +56 -0
- metadata +243 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4b14981546d524f665e1d268857bd8b2caafec9af07157a1f57fd2ca8f9c83c
|
4
|
+
data.tar.gz: 1335c436d0e4ae1f70fb70a9ef5757610e09ce3b5a03b87ba356d31625af0ab3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcaccbe3cb5e5a27cd5700e3677886fb9966ef8ce2775bf2be6638578489747b8a488acbd3f6f7f55752323c40bc92410af1d079dfeefe2d4b41d4f768fbfbfb
|
7
|
+
data.tar.gz: 23a2cbda699f9e8f2a9588dc8cfb4350bbd1a1994603577bd3649a356148ad88d2bd754529bb63905aba59a993cc747c2e5ad762038fb571d5d83b1ecc6d40b4
|
@@ -0,0 +1,107 @@
|
|
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
|
+
return @block if instance_variable_defined?(:@block)
|
63
|
+
|
64
|
+
current_block = params.fetch(:block).tap { |bl| check_block_valid(bl) if bl }
|
65
|
+
current_block ||= keyword_block
|
66
|
+
|
67
|
+
@block = current_block
|
68
|
+
end
|
69
|
+
|
70
|
+
def value(object, context)
|
71
|
+
block.call(object, context)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def keyword_block
|
77
|
+
key_method_name = key
|
78
|
+
proc { |object| object.public_send(key_method_name) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_block_valid(block)
|
82
|
+
raise Error, "Block must be a Proc (not lambda)" if block.lambda?
|
83
|
+
|
84
|
+
params = block.parameters
|
85
|
+
raise Error, "Block can have 0-2 parameters" if params.count > 2
|
86
|
+
|
87
|
+
valid_params_types = params.all? { |param| param[0] == :opt }
|
88
|
+
raise Error, "Block parameters must be optional and no keyword parameters" unless valid_params_types
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module ClassMethods
|
93
|
+
# Returns the Jat class that this Attribute class is namespaced under.
|
94
|
+
attr_accessor :jat_class
|
95
|
+
|
96
|
+
# Since Attribute is anonymously subclassed when Jat is subclassed,
|
97
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
98
|
+
# reflect the likely name for the class.
|
99
|
+
def inspect
|
100
|
+
"#{jat_class.inspect}::Attribute"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
extend ClassMethods
|
105
|
+
include InstanceMethods
|
106
|
+
end
|
107
|
+
end
|
data/lib/jat/config.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require_relative "utils/enum_deep_dup"
|
5
|
+
|
6
|
+
class Jat
|
7
|
+
class Config
|
8
|
+
module InstanceMethods
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :opts
|
12
|
+
|
13
|
+
def initialize(opts = {})
|
14
|
+
@opts = EnumDeepDup.call(opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def_delegators :@opts, :[], :[]=, :fetch
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Returns the Jat class that this config class is namespaced under.
|
22
|
+
attr_accessor :jat_class
|
23
|
+
|
24
|
+
# Since Config is anonymously subclassed when Jat is subclassed,
|
25
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
26
|
+
# reflect the likely name for the class.
|
27
|
+
def inspect
|
28
|
+
"#{jat_class.inspect}::Config"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
include InstanceMethods
|
33
|
+
extend ClassMethods
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module Activerecord
|
6
|
+
def self.plugin_name
|
7
|
+
:activerecord
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(jat_class, **opts)
|
11
|
+
if jat_class.plugin_used?(:json_api)
|
12
|
+
jat_class.plugin :json_api_activerecord, **opts
|
13
|
+
elsif jat_class.plugin_used?(:simple_api)
|
14
|
+
jat_class.plugin :simple_api_activerecord, **opts
|
15
|
+
else
|
16
|
+
raise Error, "Please load :json_api or :simple_api plugin first"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
register_plugin(Activerecord.plugin_name, Activerecord)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module Cache
|
6
|
+
def self.plugin_name
|
7
|
+
:cache
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.before_load(jat_class, **opts)
|
11
|
+
jat_class.plugin :to_str, **opts
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load(jat_class, **_opts)
|
15
|
+
jat_class.include(InstanceMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
FORMAT_TO_STR = :to_str
|
20
|
+
FORMAT_TO_H = :to_h
|
21
|
+
|
22
|
+
def to_h(object)
|
23
|
+
return super if context[:_format] == FORMAT_TO_STR
|
24
|
+
|
25
|
+
context[:_format] = FORMAT_TO_H
|
26
|
+
cached(object) { super }
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_str(object)
|
30
|
+
context[:_format] = FORMAT_TO_STR
|
31
|
+
cached(object) { super }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def cached(object, &block)
|
37
|
+
cache = context[:cache]
|
38
|
+
return yield unless cache
|
39
|
+
|
40
|
+
cache.call(object, context, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register_plugin(Cache.plugin_name, Cache)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./lib/preloader"
|
4
|
+
|
5
|
+
class Jat
|
6
|
+
module Plugins
|
7
|
+
module ActiverecordPreloads
|
8
|
+
def self.plugin_name
|
9
|
+
:_activerecord_preloads
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load(jat_class, **_opts)
|
13
|
+
jat_class.include(InstanceMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
def to_h(object)
|
18
|
+
object = add_preloads(object)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def add_preloads(obj)
|
25
|
+
return obj if obj.nil? || (obj.is_a?(Array) && obj.empty?)
|
26
|
+
|
27
|
+
# preloads() method comes from simple_api_activerecord or json_api_activerecord plugin
|
28
|
+
preloads = preloads()
|
29
|
+
return obj if preloads.empty?
|
30
|
+
|
31
|
+
Preloader.preload(obj, preloads)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
register_plugin(ActiverecordPreloads.plugin_name, ActiverecordPreloads)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,93 @@
|
|
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 Loader
|
24
|
+
def self.call(records, associations)
|
25
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
26
|
+
ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
|
27
|
+
else
|
28
|
+
ActiveRecord::Associations::Preloader.new.preload(records, associations)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ActiverecordObject
|
34
|
+
module ClassMethods
|
35
|
+
def fit?(object)
|
36
|
+
object.is_a?(ActiveRecord::Base)
|
37
|
+
end
|
38
|
+
|
39
|
+
def preload(object, preloads)
|
40
|
+
Loader.call([object], preloads)
|
41
|
+
object
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
extend ClassMethods
|
46
|
+
end
|
47
|
+
|
48
|
+
class ActiverecordRelation
|
49
|
+
module ClassMethods
|
50
|
+
def fit?(objects)
|
51
|
+
objects.is_a?(ActiveRecord::Relation)
|
52
|
+
end
|
53
|
+
|
54
|
+
def preload(objects, preloads)
|
55
|
+
if objects.loaded?
|
56
|
+
array_objects = objects.to_a
|
57
|
+
Loader.call(array_objects, preloads)
|
58
|
+
objects
|
59
|
+
else
|
60
|
+
objects.preload(preloads).load
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
extend ClassMethods
|
66
|
+
end
|
67
|
+
|
68
|
+
class ActiverecordArray
|
69
|
+
module ClassMethods
|
70
|
+
def fit?(objects)
|
71
|
+
objects.is_a?(Array) &&
|
72
|
+
ActiverecordObject.fit?(objects.first) &&
|
73
|
+
same_kind?(objects)
|
74
|
+
end
|
75
|
+
|
76
|
+
def preload(objects, preloads)
|
77
|
+
Loader.call(objects, preloads)
|
78
|
+
objects
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def same_kind?(objects)
|
84
|
+
first_object_class = objects.first.class
|
85
|
+
objects.all? { |object| object.instance_of?(first_object_class) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
extend ClassMethods
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module LowerCamelCase
|
6
|
+
def self.plugin_name
|
7
|
+
:_lower_camel_case
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(jat_class, **_opts)
|
11
|
+
jat_class::Attribute.include(AttributeInstanceMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module AttributeInstanceMethods
|
15
|
+
def name
|
16
|
+
LowerCamelCaseTransformation.call(original_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
register_plugin(LowerCamelCase.plugin_name, LowerCamelCase)
|
22
|
+
end
|
23
|
+
|
24
|
+
class LowerCamelCaseTransformation
|
25
|
+
SEPARATOR = "_"
|
26
|
+
|
27
|
+
def self.call(string)
|
28
|
+
first_word, *others = string.to_s.split(SEPARATOR)
|
29
|
+
|
30
|
+
first_word[0] = first_word[0].downcase
|
31
|
+
last_words = others.each(&:capitalize!).join
|
32
|
+
|
33
|
+
:"#{first_word}#{last_words}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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
|
+
def self.plugin_name
|
11
|
+
:_preloads
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load(jat_class, **_opts)
|
15
|
+
jat_class::Attribute.include(AttributeMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module AttributeMethods
|
19
|
+
NULL_PRELOADS = [nil, [].freeze].freeze
|
20
|
+
|
21
|
+
def preloads
|
22
|
+
return @preloads if defined?(@preloads)
|
23
|
+
|
24
|
+
@preloads, @preloads_path = get_preloads_with_path
|
25
|
+
@preloads
|
26
|
+
end
|
27
|
+
|
28
|
+
def preloads_path
|
29
|
+
return @preloads_path if defined?(@preloads_path)
|
30
|
+
|
31
|
+
@preloads, @preloads_path = get_preloads_with_path
|
32
|
+
@preloads_path
|
33
|
+
end
|
34
|
+
|
35
|
+
# When provided multiple values in preloads, such as { user: [:profile] },
|
36
|
+
# we don't know which entity is main (:user or :profile in this example) but
|
37
|
+
# we need to know main value to add nested preloads to it.
|
38
|
+
# User can specify main preloaded entity by adding "!" suffix
|
39
|
+
# ({ user!: [:profile] } for example), otherwise the latest key will be considered main.
|
40
|
+
def get_preloads_with_path
|
41
|
+
preloads_provided = opts.key?(:preload)
|
42
|
+
preloads =
|
43
|
+
if preloads_provided
|
44
|
+
opts[:preload]
|
45
|
+
elsif relation?
|
46
|
+
key
|
47
|
+
end
|
48
|
+
|
49
|
+
# Nulls and empty hash differs as we can preload nested results to
|
50
|
+
# empty hash, but we will skip nested preloading if null or false provided
|
51
|
+
return NULL_PRELOADS if preloads_provided && !preloads
|
52
|
+
|
53
|
+
preloads = FormatUserPreloads.to_hash(preloads)
|
54
|
+
preloads, path = PreloadsWithPath.call(preloads)
|
55
|
+
|
56
|
+
[EnumDeepFreeze.call(preloads), path.freeze]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
register_plugin(Preloads.plugin_name, Preloads)
|
62
|
+
end
|
63
|
+
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
|