jat 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +21 -0
  4. data/jat.gemspec +37 -0
  5. data/lib/jat/attribute.rb +85 -0
  6. data/lib/jat/config.rb +38 -0
  7. data/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads.rb +29 -0
  8. data/lib/jat/plugins/_activerecord_preloads/lib/preloader.rb +89 -0
  9. data/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord.rb +22 -0
  10. data/lib/jat/plugins/_json_api_activerecord/lib/preloads.rb +84 -0
  11. data/lib/jat/plugins/_preloads/_preloads.rb +53 -0
  12. data/lib/jat/plugins/_preloads/lib/format_user_preloads.rb +52 -0
  13. data/lib/jat/plugins/_preloads/lib/preloads_with_path.rb +78 -0
  14. data/lib/jat/plugins/cache/cache.rb +39 -0
  15. data/lib/jat/plugins/camel_lower/camel_lower.rb +18 -0
  16. data/lib/jat/plugins/json_api/json_api.rb +207 -0
  17. data/lib/jat/plugins/json_api/lib/construct_traversal_map.rb +91 -0
  18. data/lib/jat/plugins/json_api/lib/map.rb +54 -0
  19. data/lib/jat/plugins/json_api/lib/params/fields/parse.rb +27 -0
  20. data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +55 -0
  21. data/lib/jat/plugins/json_api/lib/params/fields.rb +23 -0
  22. data/lib/jat/plugins/json_api/lib/params/include/parse.rb +55 -0
  23. data/lib/jat/plugins/json_api/lib/params/include/validate.rb +29 -0
  24. data/lib/jat/plugins/json_api/lib/params/include.rb +49 -0
  25. data/lib/jat/plugins/json_api/lib/presenters/document_links_presenter.rb +48 -0
  26. data/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter.rb +48 -0
  27. data/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter.rb +48 -0
  28. data/lib/jat/plugins/json_api/lib/presenters/links_presenter.rb +48 -0
  29. data/lib/jat/plugins/json_api/lib/presenters/meta_presenter.rb +48 -0
  30. data/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter.rb +53 -0
  31. data/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter.rb +53 -0
  32. data/lib/jat/plugins/json_api/lib/response.rb +239 -0
  33. data/lib/jat/plugins/json_api/lib/traversal_map.rb +34 -0
  34. data/lib/jat/plugins/simple_api/lib/construct_traversal_map.rb +45 -0
  35. data/lib/jat/plugins/simple_api/lib/map.rb +29 -0
  36. data/lib/jat/plugins/simple_api/lib/params/parse.rb +68 -0
  37. data/lib/jat/plugins/simple_api/lib/response.rb +134 -0
  38. data/lib/jat/plugins/simple_api/simple_api.rb +65 -0
  39. data/lib/jat/plugins/to_str/to_str.rb +44 -0
  40. data/lib/jat/plugins.rb +39 -0
  41. data/lib/jat/presenter.rb +51 -0
  42. data/lib/jat/utils/enum_deep_dup.rb +29 -0
  43. data/lib/jat/utils/enum_deep_freeze.rb +19 -0
  44. data/lib/jat.rb +66 -144
  45. data/test/lib/jat/attribute_test.rb +142 -0
  46. data/test/lib/jat/config_test.rb +57 -0
  47. data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +40 -0
  48. data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +98 -0
  49. data/test/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord_test.rb +29 -0
  50. data/test/lib/jat/plugins/_json_api_activerecord/lib/preloads_test.rb +191 -0
  51. data/test/lib/jat/plugins/_preloads/_preloads_test.rb +68 -0
  52. data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +47 -0
  53. data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +33 -0
  54. data/test/lib/jat/plugins/cache/cache_test.rb +82 -0
  55. data/test/lib/jat/plugins/camel_lower/camel_lower_test.rb +78 -0
  56. data/test/lib/jat/plugins/json_api/json_api_test.rb +154 -0
  57. data/test/lib/jat/plugins/json_api/lib/construct_traversal_map_test.rb +119 -0
  58. data/test/lib/jat/plugins/json_api/lib/map_test.rb +117 -0
  59. data/test/lib/jat/plugins/json_api/lib/params/fields/parse_test.rb +24 -0
  60. data/test/lib/jat/plugins/json_api/lib/params/fields/validate_test.rb +47 -0
  61. data/test/lib/jat/plugins/json_api/lib/params/fields_test.rb +37 -0
  62. data/test/lib/jat/plugins/json_api/lib/params/include/parse_test.rb +46 -0
  63. data/test/lib/jat/plugins/json_api/lib/params/include/validate_test.rb +51 -0
  64. data/test/lib/jat/plugins/json_api/lib/params/include_test.rb +41 -0
  65. data/test/lib/jat/plugins/json_api/lib/presenters/document_links_presenter_test.rb +69 -0
  66. data/test/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter_test.rb +69 -0
  67. data/test/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter_test.rb +69 -0
  68. data/test/lib/jat/plugins/json_api/lib/presenters/links_presenter_test.rb +69 -0
  69. data/test/lib/jat/plugins/json_api/lib/presenters/meta_presenter_test.rb +69 -0
  70. data/test/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter_test.rb +75 -0
  71. data/test/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter_test.rb +75 -0
  72. data/test/lib/jat/plugins/json_api/lib/response_test.rb +489 -0
  73. data/test/lib/jat/plugins/json_api/lib/traversal_map_test.rb +58 -0
  74. data/test/lib/jat/plugins/simple_api/lib/construct_traversal_map_test.rb +100 -0
  75. data/test/lib/jat/plugins/simple_api/lib/map_test.rb +56 -0
  76. data/test/lib/jat/plugins/simple_api/lib/params/parse_test.rb +71 -0
  77. data/test/lib/jat/plugins/simple_api/lib/response_test.rb +342 -0
  78. data/test/lib/jat/plugins/simple_api/simple_api_test.rb +81 -0
  79. data/test/lib/jat/plugins/to_str/to_str_test.rb +52 -0
  80. data/test/lib/jat/presenter_test.rb +61 -0
  81. data/test/lib/jat/utils/enum_deep_dup_test.rb +31 -0
  82. data/test/lib/jat/utils/enum_deep_freeze_test.rb +28 -0
  83. data/test/lib/jat_test.rb +120 -0
  84. data/test/lib/plugin_test.rb +49 -0
  85. data/test/support/activerecord.rb +24 -0
  86. data/test/test_helper.rb +16 -0
  87. data/test/test_plugin.rb +59 -0
  88. metadata +240 -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: 3fdf9f1733a9d7ddcdeced1bcf6b6cbb1f53b9ffa11b8cb1592874dcc55c3bf2
4
+ data.tar.gz: d8982abf27a244a845ec0147a51e23946a8f6cac66c36d350aef6567253326f8
5
5
  SHA512:
6
- metadata.gz: 369741fcd71f789f49fe0e555db75982fd7758959c59833b1b3d4dd8c55d424cd159f825a9ee6a7d17d5da3c821d44c7052b3ec49910bcf5ab569114916ed6b4
7
- data.tar.gz: 25a6235d5ec37e44917f64e0d40a98032bb391120832c0357741b56f98571d5941323b548c501bb4a1c761caf62f6cc0a65a09cb8b05f1376d20ba093799688c
6
+ metadata.gz: f8a9be4d5e0d59922b37763de59a8833b5d3c71ac634f2c3555b6b9ab36e8a4149be04f8b33a68406616cfaeda85f49ed349fe8cc4d47bc44e0a873f0128829a
7
+ data.tar.gz: 9dcf9433b3d5f40e538db79e082095b54977f8ba8fd885ada3442441c98f374635956d0cd5201c654282954dd24d59e589a181338d2774855b97bd9677fa40d4
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ### Development
2
+
3
+ * Replace `.id` DSL method with `.attribute(:id)`
4
+ * Allow to add attributes with name `:type`
5
+ * Remove possibility to add presenters methods in serializer
6
+ * Remove `delegate` option
7
+
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