jat 0.0.1 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jat/attribute.rb +107 -0
  3. data/lib/jat/config.rb +35 -0
  4. data/lib/jat/plugins/activerecord/activerecord.rb +23 -0
  5. data/lib/jat/plugins/cache/cache.rb +47 -0
  6. data/lib/jat/plugins/common/_activerecord_preloads/_activerecord_preloads.rb +38 -0
  7. data/lib/jat/plugins/common/_activerecord_preloads/lib/preloader.rb +93 -0
  8. data/lib/jat/plugins/common/_lower_camel_case/_lower_camel_case.rb +36 -0
  9. data/lib/jat/plugins/common/_preloads/_preloads.rb +63 -0
  10. data/lib/jat/plugins/common/_preloads/lib/format_user_preloads.rb +52 -0
  11. data/lib/jat/plugins/common/_preloads/lib/preloads_with_path.rb +78 -0
  12. data/lib/jat/plugins/json_api/json_api.rb +251 -0
  13. data/lib/jat/plugins/json_api/lib/fields_param_parser.rb +40 -0
  14. data/lib/jat/plugins/json_api/lib/include_param_parser.rb +84 -0
  15. data/lib/jat/plugins/json_api/lib/map.rb +119 -0
  16. data/lib/jat/plugins/json_api/lib/params/fields/parse.rb +27 -0
  17. data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +58 -0
  18. data/lib/jat/plugins/json_api/lib/params/fields.rb +23 -0
  19. data/lib/jat/plugins/json_api/lib/params/include/parse.rb +55 -0
  20. data/lib/jat/plugins/json_api/lib/params/include/validate.rb +29 -0
  21. data/lib/jat/plugins/json_api/lib/params/include.rb +49 -0
  22. data/lib/jat/plugins/json_api/lib/response.rb +123 -0
  23. data/lib/jat/plugins/json_api/lib/response_piece.rb +175 -0
  24. data/lib/jat/plugins/json_api/plugins/json_api_activerecord/json_api_activerecord.rb +23 -0
  25. data/lib/jat/plugins/json_api/plugins/json_api_lower_camel_case/json_api_lower_camel_case.rb +34 -0
  26. data/lib/jat/plugins/json_api/plugins/json_api_maps_cache/json_api_maps_cache.rb +58 -0
  27. data/lib/jat/plugins/json_api/plugins/json_api_preloads/json_api_preloads.rb +38 -0
  28. data/lib/jat/plugins/json_api/plugins/json_api_preloads/lib/preloads.rb +76 -0
  29. data/lib/jat/plugins/json_api/plugins/json_api_validate_params/json_api_validate_params.rb +61 -0
  30. data/lib/jat/plugins/json_api/plugins/json_api_validate_params/lib/params_error.rb +6 -0
  31. data/lib/jat/plugins/json_api/plugins/json_api_validate_params/lib/validate_fields_param.rb +59 -0
  32. data/lib/jat/plugins/json_api/plugins/json_api_validate_params/lib/validate_include_param.rb +33 -0
  33. data/lib/jat/plugins/lower_camel_case/lower_camel_case.rb +23 -0
  34. data/lib/jat/plugins/maps_cache/maps_cache.rb +23 -0
  35. data/lib/jat/plugins/preloads/preloads.rb +23 -0
  36. data/lib/jat/plugins/simple_api/lib/fields_param_parser.rb +97 -0
  37. data/lib/jat/plugins/simple_api/lib/map.rb +99 -0
  38. data/lib/jat/plugins/simple_api/lib/response.rb +119 -0
  39. data/lib/jat/plugins/simple_api/lib/response_piece.rb +80 -0
  40. data/lib/jat/plugins/simple_api/plugins/simple_api_activerecord/simple_api_activerecord.rb +23 -0
  41. data/lib/jat/plugins/simple_api/plugins/simple_api_lower_camel_case/simple_api_lower_camel_case.rb +34 -0
  42. data/lib/jat/plugins/simple_api/plugins/simple_api_maps_cache/simple_api_maps_cache.rb +50 -0
  43. data/lib/jat/plugins/simple_api/plugins/simple_api_preloads/lib/preloads.rb +55 -0
  44. data/lib/jat/plugins/simple_api/plugins/simple_api_preloads/simple_api_preloads.rb +38 -0
  45. data/lib/jat/plugins/simple_api/plugins/simple_api_validate_params/lib/fields_error.rb +6 -0
  46. data/lib/jat/plugins/simple_api/plugins/simple_api_validate_params/lib/validate_fields_param.rb +45 -0
  47. data/lib/jat/plugins/simple_api/plugins/simple_api_validate_params/simple_api_validate_params.rb +49 -0
  48. data/lib/jat/plugins/simple_api/simple_api.rb +125 -0
  49. data/lib/jat/plugins/to_str/to_str.rb +54 -0
  50. data/lib/jat/plugins/types/types.rb +54 -0
  51. data/lib/jat/plugins/validate_params/validate_params.rb +23 -0
  52. data/lib/jat/plugins.rb +39 -0
  53. data/lib/jat/utils/enum_deep_dup.rb +29 -0
  54. data/lib/jat/utils/enum_deep_freeze.rb +19 -0
  55. data/lib/jat.rb +66 -141
  56. data/test/lib/jat/attribute_test.rb +152 -0
  57. data/test/lib/jat/config_test.rb +57 -0
  58. data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +59 -0
  59. data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +84 -0
  60. data/test/lib/jat/plugins/_camel_lower/_camel_lower_test.rb +26 -0
  61. data/test/lib/jat/plugins/_preloads/_preloads_test.rb +68 -0
  62. data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +47 -0
  63. data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +33 -0
  64. data/test/lib/jat/plugins/cache/cache_test.rb +82 -0
  65. data/test/lib/jat/plugins/json_api/json_api_test.rb +162 -0
  66. data/test/lib/jat/plugins/json_api/lib/fields_param_parser_test.rb +38 -0
  67. data/test/lib/jat/plugins/json_api/lib/include_param_parser_test.rb +41 -0
  68. data/test/lib/jat/plugins/json_api/lib/map_test.rb +188 -0
  69. data/test/lib/jat/plugins/json_api/lib/response_test.rb +489 -0
  70. data/test/lib/jat/plugins/json_api_activerecord/json_api_activerecord_test.rb +24 -0
  71. data/test/lib/jat/plugins/json_api_camel_lower/json_api_camel_lower_test.rb +79 -0
  72. data/test/lib/jat/plugins/json_api_maps_cache/json_api_maps_cache_test.rb +107 -0
  73. data/test/lib/jat/plugins/json_api_preloads/json_api_preloads_test.rb +37 -0
  74. data/test/lib/jat/plugins/json_api_preloads/lib/preloads_test.rb +197 -0
  75. data/test/lib/jat/plugins/json_api_validate_params/json_api_validate_params_test.rb +84 -0
  76. data/test/lib/jat/plugins/simple_api/lib/fields_param_parser_test.rb +77 -0
  77. data/test/lib/jat/plugins/simple_api/lib/map_test.rb +133 -0
  78. data/test/lib/jat/plugins/simple_api/lib/response_test.rb +348 -0
  79. data/test/lib/jat/plugins/simple_api/simple_api_test.rb +139 -0
  80. data/test/lib/jat/plugins/simple_api_activerecord/simple_api_activerecord_test.rb +24 -0
  81. data/test/lib/jat/plugins/simple_api_camel_lower/simple_api_camel_lower_test.rb +48 -0
  82. data/test/lib/jat/plugins/simple_api_maps_cache/simple_api_maps_cache_test.rb +95 -0
  83. data/test/lib/jat/plugins/simple_api_preloads/lib/preloads_test.rb +140 -0
  84. data/test/lib/jat/plugins/simple_api_preloads/simple_api_preloads_test.rb +37 -0
  85. data/test/lib/jat/plugins/simple_api_validate_params/simple_api_validate_params_test.rb +89 -0
  86. data/test/lib/jat/plugins/to_str/to_str_test.rb +52 -0
  87. data/test/lib/jat/plugins/types/types_test.rb +79 -0
  88. data/test/lib/jat/utils/enum_deep_dup_test.rb +31 -0
  89. data/test/lib/jat/utils/enum_deep_freeze_test.rb +28 -0
  90. data/test/lib/jat_test.rb +143 -0
  91. data/test/lib/plugin_test.rb +49 -0
  92. data/test/support/activerecord.rb +24 -0
  93. data/test/test_helper.rb +13 -0
  94. data/test/test_plugin.rb +56 -0
  95. metadata +243 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c17f2cb00d5dc319df32abc1b0fd41d00adb30f0f8357faf6d74b7d440ef1d9
4
- data.tar.gz: 0d638bef47aa94698fa25a5c2dc0f6559e528ef8b5bf525bec07310307dbcda3
3
+ metadata.gz: e4b14981546d524f665e1d268857bd8b2caafec9af07157a1f57fd2ca8f9c83c
4
+ data.tar.gz: 1335c436d0e4ae1f70fb70a9ef5757610e09ce3b5a03b87ba356d31625af0ab3
5
5
  SHA512:
6
- metadata.gz: 369741fcd71f789f49fe0e555db75982fd7758959c59833b1b3d4dd8c55d424cd159f825a9ee6a7d17d5da3c821d44c7052b3ec49910bcf5ab569114916ed6b4
7
- data.tar.gz: 25a6235d5ec37e44917f64e0d40a98032bb391120832c0357741b56f98571d5941323b548c501bb4a1c761caf62f6cc0a65a09cb8b05f1376d20ba093799688c
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