jat 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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