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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module JsonApiValidateParams
6
+ class ValidateFieldsParam
7
+ def self.call(jat_class, fields)
8
+ full_map = jat_class.map_full
9
+
10
+ fields.each do |type, attributes_names|
11
+ new(type, full_map).validate(attributes_names)
12
+ end
13
+ end
14
+
15
+ attr_reader :type, :full_map
16
+
17
+ def initialize(type, full_map)
18
+ @type = type
19
+ @full_map = full_map
20
+ end
21
+
22
+ def validate(attributes_names)
23
+ check_fields_type
24
+ check_attributes_names(attributes_names)
25
+ end
26
+
27
+ private
28
+
29
+ def check_fields_type
30
+ return if full_map.key?(type)
31
+
32
+ allowed_types = "'#{full_map.keys.join("', '")}'"
33
+
34
+ raise JsonApiParamsError, <<~ERROR.strip
35
+ Response does not have resources with type '#{type}'. Existing types are: #{allowed_types}
36
+ ERROR
37
+ end
38
+
39
+ def check_attributes_names(attributes_names)
40
+ attributes_names.each do |attribute_name|
41
+ check_attribute_name(attribute_name)
42
+ end
43
+ end
44
+
45
+ def check_attribute_name(attribute_name)
46
+ type_data = full_map.fetch(type)
47
+ type_serializer = type_data.fetch(:serializer)
48
+ return if type_serializer.attributes.key?(attribute_name)
49
+
50
+ allowed_attributes = "'#{type_serializer.attributes.keys.join("', '")}'"
51
+
52
+ raise JsonApiParamsError, <<~ERROR.strip
53
+ No attribute '#{attribute_name}' in resource type '#{type}'. Existing attributes are: #{allowed_attributes}
54
+ ERROR
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module JsonApiValidateParams
6
+ class ValidateIncludeParam
7
+ class << self
8
+ def call(jat_class, includes)
9
+ includes.each do |name, nested_includes|
10
+ attribute = jat_class.attributes[name]
11
+ raise_error(jat_class, name) if !attribute || !attribute.relation?
12
+
13
+ nested_serializer = attribute.serializer.call
14
+ call(nested_serializer, nested_includes)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def raise_error(jat_class, name)
21
+ type = jat_class.get_type
22
+ allowed_relationships = jat_class.attributes.each_value.select(&:relation?).map!(&:name)
23
+ allowed_relationships = "'#{allowed_relationships.join("', '")}'"
24
+
25
+ raise JsonApiParamsError,
26
+ "Type '#{type}' has no included '#{name}' relationship. " \
27
+ "Existing relationships are: #{allowed_relationships}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
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
+ if jat_class.plugin_used?(:json_api)
12
+ jat_class.plugin :json_api_lower_camel_case, **opts
13
+ elsif jat_class.plugin_used?(:simple_api)
14
+ jat_class.plugin :simple_api_lower_camel_case, **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(LowerCamelCase.plugin_name, LowerCamelCase)
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module MapsCache
6
+ def self.plugin_name
7
+ :maps_cache
8
+ end
9
+
10
+ def self.load(jat_class, **opts)
11
+ if jat_class.plugin_used?(:json_api)
12
+ jat_class.plugin :json_api_maps_cache, **opts
13
+ elsif jat_class.plugin_used?(:simple_api)
14
+ jat_class.plugin :simple_api_maps_cache, **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(MapsCache.plugin_name, MapsCache)
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module Preloads
6
+ def self.plugin_name
7
+ :preloads
8
+ end
9
+
10
+ def self.load(jat_class, **opts)
11
+ if jat_class.plugin_used?(:json_api)
12
+ jat_class.plugin :json_api_preloads, **opts
13
+ elsif jat_class.plugin_used?(:simple_api)
14
+ jat_class.plugin :simple_api_preloads, **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(Preloads.plugin_name, Preloads)
22
+ end
23
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module SimpleApi
6
+ class FieldsParamParser
7
+ module ClassMethods
8
+ # Returns the Jat class that this FieldsParamParser class is namespaced under.
9
+ attr_accessor :jat_class
10
+
11
+ # Since FieldsParamParser is anonymously subclassed when Jat is subclassed,
12
+ # and then assigned to a constant of the Jat subclass, make inspect
13
+ # reflect the likely name for the class.
14
+ def inspect
15
+ "#{jat_class.inspect}::FieldsParamParser"
16
+ end
17
+
18
+ def parse(fields)
19
+ return FROZEN_EMPTY_HASH unless fields
20
+
21
+ new(fields).parse
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ COMMA = ","
27
+ OPEN_BRACKET = "("
28
+ CLOSE_BRACKET = ")"
29
+
30
+ def initialize(fields)
31
+ @fields = fields
32
+ end
33
+
34
+ # user => { user: {} }
35
+ # user(id) => { user: { id: {} } }
36
+ # user(id,name) => { user: { id: {}, name: {} } }
37
+ # user,comments => { user: {}, comments: {} }
38
+ # user(comments(text)) => { user: { comments: { text: {} } } }
39
+ def parse
40
+ current_attr = nil
41
+
42
+ fields.each_char do |char|
43
+ case char
44
+ when COMMA
45
+ next unless current_attr
46
+
47
+ add_attribute(current_attr)
48
+ current_attr = nil
49
+ when CLOSE_BRACKET
50
+ if current_attr
51
+ add_attribute(current_attr)
52
+ current_attr = nil
53
+ end
54
+
55
+ route.pop
56
+ when OPEN_BRACKET
57
+ next unless current_attr
58
+
59
+ attribute_name = add_attribute(current_attr, {})
60
+ route << attribute_name
61
+ current_attr = nil
62
+ else
63
+ current_attr = current_attr ? current_attr.insert(-1, char) : char
64
+ end
65
+ end
66
+
67
+ add_attribute(current_attr) if current_attr
68
+
69
+ res
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :fields
75
+
76
+ def add_attribute(current_attr, nested_attrs = FROZEN_EMPTY_HASH)
77
+ current_resource = route.empty? ? res : res.dig(*route)
78
+ attribute_name = current_attr.strip.to_sym
79
+ current_resource[attribute_name] = nested_attrs
80
+ attribute_name
81
+ end
82
+
83
+ def res
84
+ @res ||= {}
85
+ end
86
+
87
+ def route
88
+ @route ||= []
89
+ end
90
+ end
91
+
92
+ extend ClassMethods
93
+ include InstanceMethods
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module SimpleApi
6
+ class Map
7
+ module ClassMethods
8
+ # Returns the Jat class that this Map class is namespaced under.
9
+ attr_accessor :jat_class
10
+
11
+ # Since Map is anonymously subclassed when Jat is subclassed,
12
+ # and then assigned to a constant of the Jat subclass, make inspect
13
+ # reflect the likely name for the class.
14
+ def inspect
15
+ "#{jat_class.inspect}::Map"
16
+ end
17
+
18
+ # Returns structure like
19
+ # {
20
+ # key1 => { key11 => {}, key12 => { ... } },
21
+ # key2 => { key21 => {}, key22 => { ... } },
22
+ # }
23
+ def call(context)
24
+ exposed = context[:exposed]&.to_sym || :default
25
+ fields = context[:fields]
26
+
27
+ construct_map(exposed, fields)
28
+ end
29
+
30
+ private
31
+
32
+ def construct_map(exposed, fields)
33
+ fields = jat_class::FieldsParamParser.parse(fields) if fields
34
+ new(exposed, fields).to_h
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ attr_reader :exposed, :fields
40
+
41
+ EXPOSED_TYPES = {all: :all, default: :default, none: :none}.freeze
42
+
43
+ def initialize(exposed, fields)
44
+ @exposed = EXPOSED_TYPES.fetch(exposed)
45
+ @fields = fields
46
+ end
47
+
48
+ def to_h
49
+ map_for(self.class.jat_class, fields)
50
+ end
51
+
52
+ def map_for(serializer, fields, stack = [])
53
+ serializer.attributes.each_with_object({}) do |name_attr, result|
54
+ name = name_attr[0]
55
+ attribute = name_attr[1]
56
+ next unless expose?(attribute, fields)
57
+
58
+ raise Error, recursive_error_message(stack, name) if stack.any?(name_attr)
59
+ stack << name_attr
60
+
61
+ result[name] =
62
+ if attribute.relation?
63
+ nested_serializer = attribute.serializer.call
64
+ nested_fields = fields&.[](name)
65
+ map_for(nested_serializer, nested_fields, stack)
66
+ else
67
+ FROZEN_EMPTY_HASH
68
+ end
69
+
70
+ stack.pop
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def expose?(attribute, fields)
77
+ case exposed
78
+ when :all then true
79
+ when :none then manually_exposed?(attribute, fields)
80
+ else attribute.exposed? || manually_exposed?(attribute, fields)
81
+ end
82
+ end
83
+
84
+ def manually_exposed?(attribute, fields)
85
+ fields&.include?(attribute.name)
86
+ end
87
+
88
+ def recursive_error_message(stack, name)
89
+ recursion = (stack.map!(&:first) << name).join(" -> ")
90
+ "Recursive serialization: #{recursion}"
91
+ end
92
+ end
93
+
94
+ extend ClassMethods
95
+ include InstanceMethods
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Serializes to JSON-API format
4
+ class Jat
5
+ module Plugins
6
+ module SimpleApi
7
+ class Response
8
+ module ClassMethods
9
+ # Returns the Jat class that this Response class is namespaced under.
10
+ attr_accessor :jat_class
11
+
12
+ # Since Response is anonymously subclassed when Jat is subclassed,
13
+ # and then assigned to a constant of the Jat subclass, make inspect
14
+ # reflect the likely name for the class.
15
+ def inspect
16
+ "#{jat_class.inspect}::Response"
17
+ end
18
+
19
+ def call(object, context)
20
+ new(object, context).to_h
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ attr_reader :object, :context, :jat_class
26
+
27
+ def initialize(object, context)
28
+ @object = object
29
+ @context = context
30
+ @jat_class = self.class.jat_class
31
+ end
32
+
33
+ def to_h
34
+ # Add main response
35
+ is_many = many?
36
+ root = root_key(is_many)
37
+
38
+ response = is_many ? many(object) : one(object)
39
+ response = {root => response} if root
40
+ response ||= {}
41
+
42
+ add_metadata(response, root)
43
+
44
+ response
45
+ end
46
+
47
+ private
48
+
49
+ def many(objects)
50
+ objects.map { |obj| one(obj) }
51
+ end
52
+
53
+ def one(obj)
54
+ map = jat_class.map(context)
55
+ jat_class::ResponsePiece.to_h(obj, context, map)
56
+ end
57
+
58
+ def many?
59
+ many = context[:many]
60
+ many.nil? ? object.is_a?(Enumerable) : many
61
+ end
62
+
63
+ # We can provide nil or false to remove root
64
+ def root_key(is_many)
65
+ if context.key?(:root)
66
+ root = context[:root]
67
+ root ? root.to_sym : root
68
+ else
69
+ config = jat_class.config
70
+ is_many ? config[:root_many] : config[:root_one]
71
+ end
72
+ end
73
+
74
+ # Add metadata to response
75
+ # We can add metadata whether to empty response or to top-level namespace
76
+ # We should not mix metadata with object attributes
77
+ def add_metadata(response, root)
78
+ meta = metadata
79
+ return if meta.empty?
80
+
81
+ raise Error, "Response must have a root key to add metadata" if !response.empty? && !root
82
+ response[meta_key] = meta
83
+ end
84
+
85
+ def meta_key
86
+ context[:meta_key]&.to_sym || jat_class.config[:meta_key]
87
+ end
88
+
89
+ def metadata
90
+ data = context_metadata
91
+
92
+ meta = jat_class.added_meta
93
+ return data if meta.empty?
94
+
95
+ meta.each do |name, attribute|
96
+ next if data.key?(name)
97
+
98
+ value = attribute.value(object, context)
99
+
100
+ unless value.nil?
101
+ data = data.dup if data.equal?(FROZEN_EMPTY_HASH)
102
+ data[name] = value
103
+ end
104
+ end
105
+
106
+ data
107
+ end
108
+
109
+ def context_metadata
110
+ context[:meta]&.transform_keys(&:to_sym) || FROZEN_EMPTY_HASH
111
+ end
112
+ end
113
+
114
+ extend ClassMethods
115
+ include InstanceMethods
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Serializes to JSON-API format
4
+ class Jat
5
+ module Plugins
6
+ module SimpleApi
7
+ class ResponsePiece
8
+ module ClassMethods
9
+ # Returns the Jat class that this ResponsePiece class is namespaced under.
10
+ attr_accessor :jat_class
11
+
12
+ # Since ResponsePiece is anonymously subclassed when Jat is subclassed,
13
+ # and then assigned to a constant of the Jat subclass, make inspect
14
+ # reflect the likely name for the class.
15
+ def inspect
16
+ "#{jat_class.inspect}::ResponsePiece"
17
+ end
18
+
19
+ def to_h(object, context, map)
20
+ new(object, context).piece(map)
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ attr_reader :jat_class, :object, :context
26
+
27
+ def initialize(object, context)
28
+ @object = object
29
+ @context = context
30
+ @jat_class = self.class.jat_class
31
+ end
32
+
33
+ def piece(map)
34
+ return unless object
35
+
36
+ result = {}
37
+
38
+ map.each do |key, inner_map|
39
+ attribute = jat_class.attributes.fetch(key)
40
+ value = attribute.value(object, context)
41
+
42
+ result[key] =
43
+ if attribute.relation?
44
+ if many?(attribute, value)
45
+ value.map { |obj| inner_piece(attribute, obj, inner_map) }
46
+ else
47
+ inner_piece(attribute, value, inner_map)
48
+ end
49
+ else
50
+ value
51
+ end
52
+ end
53
+
54
+ result
55
+ end
56
+
57
+ private
58
+
59
+ def inner_piece(attribute, value, inner_map)
60
+ serializer = attribute.serializer.call
61
+ serializer::ResponsePiece.to_h(value, context, inner_map)
62
+ end
63
+
64
+ def many?(attribute, nested_object)
65
+ is_many = attribute.many?
66
+
67
+ # handle boolean
68
+ return is_many if (is_many == true) || (is_many == false)
69
+
70
+ # handle nil
71
+ nested_object.is_a?(Enumerable)
72
+ end
73
+ end
74
+
75
+ extend ClassMethods
76
+ include InstanceMethods
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module SimpleApiActiverecord
6
+ def self.plugin_name
7
+ :simple_api_activerecord
8
+ end
9
+
10
+ def self.before_load(jat_class, **_opts)
11
+ return if jat_class.plugin_used?(:simple_api)
12
+ raise Error, "Please load :simple_api plugin first"
13
+ end
14
+
15
+ def self.load(jat_class, **opts)
16
+ jat_class.plugin :simple_api_preloads, **opts
17
+ jat_class.plugin :_activerecord_preloads, **opts
18
+ end
19
+ end
20
+
21
+ register_plugin(SimpleApiActiverecord.plugin_name, SimpleApiActiverecord)
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module SimpleApiLowerCamelCase
6
+ def self.plugin_name
7
+ :simple_api_lower_camel_case
8
+ end
9
+
10
+ def self.before_load(jat_class, **_opts)
11
+ raise Error, "Please load :simple_api plugin first" unless jat_class.plugin_used?(:simple_api)
12
+
13
+ jat_class.plugin :_lower_camel_case
14
+ end
15
+
16
+ def self.load(jat_class, **_opts)
17
+ jat_class::Response.include(ResponseInstanceMethods)
18
+ end
19
+
20
+ module ResponseInstanceMethods
21
+ private
22
+
23
+ def context_metadata
24
+ metadata = super
25
+ return metadata if metadata.empty?
26
+
27
+ metadata.transform_keys! { |key| Jat::LowerCamelCaseTransformation.call(key) }
28
+ end
29
+ end
30
+ end
31
+
32
+ register_plugin(SimpleApiLowerCamelCase.plugin_name, SimpleApiLowerCamelCase)
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module SimpleApiMapsCache
6
+ def self.plugin_name
7
+ :simple_api_maps_cache
8
+ end
9
+
10
+ def self.before_load(jat_class, **_opts)
11
+ return if jat_class.plugin_used?(:simple_api)
12
+
13
+ raise Error, "Please load :simple_api plugin first"
14
+ end
15
+
16
+ def self.load(jat_class, **_opts)
17
+ jat_class::Map.extend(MapsCacheClassMethods)
18
+ end
19
+
20
+ def self.after_load(jat_class, **opts)
21
+ jat_class.config[:cached_maps_count] = opts[:cached_maps_count] || 100
22
+ end
23
+
24
+ module MapsCacheClassMethods
25
+ # Caches up to `:cached_maps_count` maps for each serializer.
26
+ # Removes earliest value if new value exceeds limit.
27
+ def maps_cache
28
+ @maps_cache ||= Hash.new do |cache, cache_key|
29
+ cache.shift if cache.length >= jat_class.config[:cached_maps_count] # protect from memory leak
30
+ cache[cache_key] = EnumDeepFreeze.call(yield)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def construct_map(exposed, fields)
37
+ cache = maps_cache { super }
38
+ cache_key = cache_key(exposed, fields)
39
+ cache[cache_key]
40
+ end
41
+
42
+ def cache_key(exposed, fields)
43
+ "exposed:#{exposed}:fields:#{fields}:"
44
+ end
45
+ end
46
+ end
47
+
48
+ register_plugin(SimpleApiMapsCache.plugin_name, SimpleApiMapsCache)
49
+ end
50
+ end