jat 0.0.1 → 0.0.7

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.
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