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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +21 -0
- data/jat.gemspec +37 -0
- data/lib/jat/attribute.rb +85 -0
- data/lib/jat/config.rb +38 -0
- data/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads.rb +29 -0
- data/lib/jat/plugins/_activerecord_preloads/lib/preloader.rb +89 -0
- data/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord.rb +22 -0
- data/lib/jat/plugins/_json_api_activerecord/lib/preloads.rb +84 -0
- data/lib/jat/plugins/_preloads/_preloads.rb +53 -0
- data/lib/jat/plugins/_preloads/lib/format_user_preloads.rb +52 -0
- data/lib/jat/plugins/_preloads/lib/preloads_with_path.rb +78 -0
- data/lib/jat/plugins/cache/cache.rb +39 -0
- data/lib/jat/plugins/camel_lower/camel_lower.rb +18 -0
- data/lib/jat/plugins/json_api/json_api.rb +207 -0
- data/lib/jat/plugins/json_api/lib/construct_traversal_map.rb +91 -0
- data/lib/jat/plugins/json_api/lib/map.rb +54 -0
- data/lib/jat/plugins/json_api/lib/params/fields/parse.rb +27 -0
- data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +55 -0
- data/lib/jat/plugins/json_api/lib/params/fields.rb +23 -0
- data/lib/jat/plugins/json_api/lib/params/include/parse.rb +55 -0
- data/lib/jat/plugins/json_api/lib/params/include/validate.rb +29 -0
- data/lib/jat/plugins/json_api/lib/params/include.rb +49 -0
- data/lib/jat/plugins/json_api/lib/presenters/document_links_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/links_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/meta_presenter.rb +48 -0
- data/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter.rb +53 -0
- data/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter.rb +53 -0
- data/lib/jat/plugins/json_api/lib/response.rb +239 -0
- data/lib/jat/plugins/json_api/lib/traversal_map.rb +34 -0
- data/lib/jat/plugins/simple_api/lib/construct_traversal_map.rb +45 -0
- data/lib/jat/plugins/simple_api/lib/map.rb +29 -0
- data/lib/jat/plugins/simple_api/lib/params/parse.rb +68 -0
- data/lib/jat/plugins/simple_api/lib/response.rb +134 -0
- data/lib/jat/plugins/simple_api/simple_api.rb +65 -0
- data/lib/jat/plugins/to_str/to_str.rb +44 -0
- data/lib/jat/plugins.rb +39 -0
- data/lib/jat/presenter.rb +51 -0
- data/lib/jat/utils/enum_deep_dup.rb +29 -0
- data/lib/jat/utils/enum_deep_freeze.rb +19 -0
- data/lib/jat.rb +66 -144
- data/test/lib/jat/attribute_test.rb +142 -0
- data/test/lib/jat/config_test.rb +57 -0
- data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +40 -0
- data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +98 -0
- data/test/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord_test.rb +29 -0
- data/test/lib/jat/plugins/_json_api_activerecord/lib/preloads_test.rb +191 -0
- data/test/lib/jat/plugins/_preloads/_preloads_test.rb +68 -0
- data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +47 -0
- data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +33 -0
- data/test/lib/jat/plugins/cache/cache_test.rb +82 -0
- data/test/lib/jat/plugins/camel_lower/camel_lower_test.rb +78 -0
- data/test/lib/jat/plugins/json_api/json_api_test.rb +154 -0
- data/test/lib/jat/plugins/json_api/lib/construct_traversal_map_test.rb +119 -0
- data/test/lib/jat/plugins/json_api/lib/map_test.rb +117 -0
- data/test/lib/jat/plugins/json_api/lib/params/fields/parse_test.rb +24 -0
- data/test/lib/jat/plugins/json_api/lib/params/fields/validate_test.rb +47 -0
- data/test/lib/jat/plugins/json_api/lib/params/fields_test.rb +37 -0
- data/test/lib/jat/plugins/json_api/lib/params/include/parse_test.rb +46 -0
- data/test/lib/jat/plugins/json_api/lib/params/include/validate_test.rb +51 -0
- data/test/lib/jat/plugins/json_api/lib/params/include_test.rb +41 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/document_links_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/links_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/meta_presenter_test.rb +69 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter_test.rb +75 -0
- data/test/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter_test.rb +75 -0
- data/test/lib/jat/plugins/json_api/lib/response_test.rb +489 -0
- data/test/lib/jat/plugins/json_api/lib/traversal_map_test.rb +58 -0
- data/test/lib/jat/plugins/simple_api/lib/construct_traversal_map_test.rb +100 -0
- data/test/lib/jat/plugins/simple_api/lib/map_test.rb +56 -0
- data/test/lib/jat/plugins/simple_api/lib/params/parse_test.rb +71 -0
- data/test/lib/jat/plugins/simple_api/lib/response_test.rb +342 -0
- data/test/lib/jat/plugins/simple_api/simple_api_test.rb +81 -0
- data/test/lib/jat/plugins/to_str/to_str_test.rb +52 -0
- data/test/lib/jat/presenter_test.rb +61 -0
- data/test/lib/jat/utils/enum_deep_dup_test.rb +31 -0
- data/test/lib/jat/utils/enum_deep_freeze_test.rb +28 -0
- data/test/lib/jat_test.rb +120 -0
- data/test/lib/plugin_test.rb +49 -0
- data/test/support/activerecord.rb +24 -0
- data/test/test_helper.rb +16 -0
- data/test/test_plugin.rb +59 -0
- metadata +240 -11
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
attr_reader :jat, :jat_class, :object, :context
|
|
9
|
+
|
|
10
|
+
def initialize(jat)
|
|
11
|
+
@jat = jat
|
|
12
|
+
@jat_class = jat.class
|
|
13
|
+
@object = jat.object
|
|
14
|
+
@context = jat.context
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def response
|
|
18
|
+
# Add main response
|
|
19
|
+
result = many? ? many(object) : one(object)
|
|
20
|
+
result = {root_key => result} if root_key
|
|
21
|
+
result ||= {}
|
|
22
|
+
|
|
23
|
+
# Add metadata to response
|
|
24
|
+
# We can add metadata to empty response, or to top-level namespace
|
|
25
|
+
# We should not mix metadata with object attributes
|
|
26
|
+
metadata.tap do |meta|
|
|
27
|
+
next if meta.empty?
|
|
28
|
+
raise Error, "Response must have a root key to add metadata" if !result.empty? && !root_key
|
|
29
|
+
result[meta_key] = meta
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def metadata
|
|
38
|
+
result = context[:meta] || {}
|
|
39
|
+
|
|
40
|
+
config_meta = jat_class.config[:meta]
|
|
41
|
+
return result unless config_meta
|
|
42
|
+
|
|
43
|
+
config_meta.each_with_object(result) do |(key, value), res|
|
|
44
|
+
next if res.key?(key) # do not overwrite manually added meta
|
|
45
|
+
|
|
46
|
+
value = value.call(object, context) if value.respond_to?(:call)
|
|
47
|
+
res[key] = value unless value.nil?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def many(objects)
|
|
52
|
+
objects.map { |obj| one(obj) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def one(obj)
|
|
56
|
+
ResponseData.new(jat_class, obj, context, jat.traversal_map).data
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def many?
|
|
60
|
+
@is_many ||= begin
|
|
61
|
+
many = context[:many]
|
|
62
|
+
many.nil? ? object.is_a?(Enumerable) : many
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# We can provide nil or false to remove root
|
|
67
|
+
def root_key
|
|
68
|
+
@root_key ||=
|
|
69
|
+
if context.key?(:root)
|
|
70
|
+
context[:root]
|
|
71
|
+
else
|
|
72
|
+
(many? ? jat_class.root_for_many : jat_class.root_for_one) || jat_class.root
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def meta_key
|
|
77
|
+
context[:meta_key] || jat_class.meta_key
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class ResponseData
|
|
82
|
+
attr_reader :jat_class, :object, :context, :map, :presenter
|
|
83
|
+
|
|
84
|
+
def initialize(jat_class, object, context, map)
|
|
85
|
+
@jat_class = jat_class
|
|
86
|
+
@object = object
|
|
87
|
+
@context = context
|
|
88
|
+
@map = map
|
|
89
|
+
@presenter = jat_class::Presenter.new(object, context)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def data
|
|
93
|
+
return unless object
|
|
94
|
+
|
|
95
|
+
result = {}
|
|
96
|
+
|
|
97
|
+
map.each do |key, inner_keys|
|
|
98
|
+
attribute = jat_class.attributes.fetch(key)
|
|
99
|
+
value = presenter.public_send(attribute.original_name)
|
|
100
|
+
|
|
101
|
+
result[key] =
|
|
102
|
+
if attribute.relation?
|
|
103
|
+
if many?(attribute, value)
|
|
104
|
+
value.map { |obj| response_data(attribute, obj, inner_keys) }
|
|
105
|
+
else
|
|
106
|
+
response_data(attribute, value, inner_keys)
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
result
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def response_data(attribute, value, map)
|
|
119
|
+
ResponseData.new(attribute.serializer.call, value, context, map).data
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def many?(attribute, object)
|
|
123
|
+
is_many = attribute.many?
|
|
124
|
+
|
|
125
|
+
# handle boolean
|
|
126
|
+
return is_many if (is_many == true) || (is_many == false)
|
|
127
|
+
|
|
128
|
+
# handle nil
|
|
129
|
+
object.is_a?(Enumerable)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "./lib/map"
|
|
4
|
+
require_relative "./lib/response"
|
|
5
|
+
|
|
6
|
+
# Serializes to JSON-API format
|
|
7
|
+
class Jat
|
|
8
|
+
module Plugins
|
|
9
|
+
module SimpleApi
|
|
10
|
+
DEFAULT_META_KEY = :meta
|
|
11
|
+
|
|
12
|
+
def self.after_load(jat_class, **opts)
|
|
13
|
+
jat_class.plugin(:_json_api_activerecord, **opts) if opts[:activerecord]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module InstanceMethods
|
|
17
|
+
def to_h
|
|
18
|
+
Response.new(self).response
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def traversal_map
|
|
22
|
+
@traversal_map ||= Map.call(self)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module ClassMethods
|
|
27
|
+
def inherited(subclass)
|
|
28
|
+
subclass.root(@root) if defined?(@root)
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def root(new_root = nil)
|
|
34
|
+
return (defined?(@root) && @root) unless new_root
|
|
35
|
+
|
|
36
|
+
new_root = new_root.to_sym
|
|
37
|
+
@root = new_root
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def root_for_one(new_root_for_one = nil)
|
|
41
|
+
return (defined?(@root_for_one) && @root_for_one) unless new_root_for_one
|
|
42
|
+
|
|
43
|
+
new_root_for_one = new_root_for_one.to_sym
|
|
44
|
+
@root_for_one = new_root_for_one
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def root_for_many(new_root_for_many = nil)
|
|
48
|
+
return (defined?(@root_for_many) && @root_for_many) unless new_root_for_many
|
|
49
|
+
|
|
50
|
+
new_root_for_many = new_root_for_many.to_sym
|
|
51
|
+
@root_for_many = new_root_for_many
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def meta_key(new_meta_key = nil)
|
|
55
|
+
return ((defined?(@meta_key) && @meta_key) || DEFAULT_META_KEY) unless new_meta_key
|
|
56
|
+
|
|
57
|
+
new_meta_key = new_meta_key.to_sym
|
|
58
|
+
@meta_key = new_meta_key
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
register_plugin(:simple_api, SimpleApi)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Jat
|
|
4
|
+
module Plugins
|
|
5
|
+
module ToStr
|
|
6
|
+
def self.after_load(jat_class, **opts)
|
|
7
|
+
jat_class.config[:to_str] = opts[:to_str] || ->(data) { ToStrJSON.dump(data) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def to_str(object, context = {})
|
|
12
|
+
new(object, context).to_str
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module InstanceMethods
|
|
17
|
+
def to_str
|
|
18
|
+
config[:to_str].call(to_h)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class ToStrJSON
|
|
23
|
+
module ClassMethods
|
|
24
|
+
def dump(response)
|
|
25
|
+
json_adapter.dump(response)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def json_adapter
|
|
31
|
+
@json_adapter ||= begin
|
|
32
|
+
require "json"
|
|
33
|
+
::JSON
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
extend ClassMethods
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
register_plugin(:to_str, ToStr)
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/jat/plugins.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Jat
|
|
4
|
+
module Plugins
|
|
5
|
+
@plugins = {}
|
|
6
|
+
|
|
7
|
+
# Register the given plugin with Jat, so that it can be loaded using
|
|
8
|
+
# `Jat.plugin` with a symbol. Should be used by plugin files. Example:
|
|
9
|
+
#
|
|
10
|
+
# Jat::Plugins.register_plugin(:plugin_name, PluginModule)
|
|
11
|
+
def self.register_plugin(name, mod)
|
|
12
|
+
@plugins[name] = mod
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# If the registered plugin already exists, use it. Otherwise, require it
|
|
16
|
+
# and return it. This raises a LoadError if such a plugin doesn't exist,
|
|
17
|
+
# or a Jat::Error if it exists but it does not register itself
|
|
18
|
+
# correctly.
|
|
19
|
+
def self.load_plugin(name)
|
|
20
|
+
require "jat/plugins/#{name}/#{name}" unless @plugins.key?(name)
|
|
21
|
+
|
|
22
|
+
@plugins[name] || raise(Error, "plugin #{name} did not register itself correctly in Jat::Plugins")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Delegate call to the plugin in a way that works across Ruby versions.
|
|
26
|
+
def self.before_load(plugin, jat_class, **opts)
|
|
27
|
+
return unless plugin.respond_to?(:before_load)
|
|
28
|
+
|
|
29
|
+
plugin.before_load(jat_class, **opts)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Delegate call to the plugin in a way that works across Ruby versions.
|
|
33
|
+
def self.after_load(plugin, jat_class, **opts)
|
|
34
|
+
return unless plugin.respond_to?(:after_load)
|
|
35
|
+
|
|
36
|
+
plugin.after_load(jat_class, **opts)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Jat
|
|
4
|
+
# Core class that encapsulates attributes presentation logic
|
|
5
|
+
class Presenter
|
|
6
|
+
module InstanceMethods
|
|
7
|
+
# Presented object
|
|
8
|
+
attr_reader :object
|
|
9
|
+
|
|
10
|
+
# Presented context
|
|
11
|
+
attr_reader :context
|
|
12
|
+
|
|
13
|
+
def initialize(object, context)
|
|
14
|
+
@object = object
|
|
15
|
+
@context = context
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module ClassMethods
|
|
20
|
+
# Returns the Jat class that this presenter class is namespaced under.
|
|
21
|
+
attr_accessor :jat_class
|
|
22
|
+
|
|
23
|
+
# Since Presenter is anonymously subclassed when Jat is subclassed,
|
|
24
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
|
25
|
+
# reflect the likely name for the class.
|
|
26
|
+
def inspect
|
|
27
|
+
"#{jat_class.inspect}::Presenter"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_method(name, block)
|
|
31
|
+
# Warning-free method redefinition
|
|
32
|
+
remove_method(name) if method_defined?(name, false)
|
|
33
|
+
define_method(name, &block_without_args(block))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def block_without_args(block)
|
|
39
|
+
case block.parameters.count
|
|
40
|
+
when 0 then block
|
|
41
|
+
when 1 then -> { instance_exec(object, &block) }
|
|
42
|
+
when 2 then -> { instance_exec(object, context, &block) }
|
|
43
|
+
else raise Error, "Invalid block arguments count"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
extend ClassMethods
|
|
49
|
+
include InstanceMethods
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Jat
|
|
4
|
+
# Duplicates nested enumerable data
|
|
5
|
+
class EnumDeepDup
|
|
6
|
+
DUP = {
|
|
7
|
+
Hash => ->(data) { dup_hash_values(data) },
|
|
8
|
+
Array => ->(data) { dup_array_values(data) }
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def self.call(data)
|
|
12
|
+
duplicate_data = data.dup
|
|
13
|
+
DUP.fetch(duplicate_data.class).call(duplicate_data)
|
|
14
|
+
duplicate_data
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.dup_hash_values(duplicate_data)
|
|
18
|
+
duplicate_data.each do |key, value|
|
|
19
|
+
duplicate_data[key] = call(value) if value.is_a?(Enumerable)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.dup_array_values(duplicate_data)
|
|
24
|
+
duplicate_data.each_with_index do |value, index|
|
|
25
|
+
duplicate_data[index] = call(value) if value.is_a?(Enumerable)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Jat
|
|
4
|
+
# Freezes nested enumerable data
|
|
5
|
+
class EnumDeepFreeze
|
|
6
|
+
FREEZES = {
|
|
7
|
+
Hash => ->(data) { data.each_value(&FREEZE_ENUMS) },
|
|
8
|
+
Array => ->(data) { data.each(&FREEZE_ENUMS) }
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
FREEZE_ENUMS = ->(value) { call(value) if value.is_a?(Enumerable) }
|
|
12
|
+
|
|
13
|
+
def self.call(data)
|
|
14
|
+
data.freeze
|
|
15
|
+
FREEZES.fetch(data.class).call(data)
|
|
16
|
+
data
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/jat.rb
CHANGED
|
@@ -1,186 +1,108 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
require 'jat/map/construct'
|
|
8
|
-
require 'jat/opts'
|
|
9
|
-
require 'jat/preloads'
|
|
10
|
-
require 'jat/preload_handler'
|
|
11
|
-
require 'jat/response'
|
|
12
|
-
require 'jat/utils/preloads_to_hash'
|
|
3
|
+
require_relative "jat/attribute"
|
|
4
|
+
require_relative "jat/presenter"
|
|
5
|
+
require_relative "jat/config"
|
|
6
|
+
require_relative "jat/plugins"
|
|
13
7
|
|
|
14
8
|
# Main namespace
|
|
15
9
|
class Jat
|
|
10
|
+
# A generic exception used by Jat.
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
@config = Config.new
|
|
14
|
+
|
|
16
15
|
module ClassMethods
|
|
16
|
+
attr_reader :config
|
|
17
|
+
|
|
17
18
|
def inherited(subclass)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# Initialize config
|
|
20
|
+
config_class = Class.new(self::Config)
|
|
21
|
+
config_class.jat_class = subclass
|
|
22
|
+
subclass.const_set(:Config, config_class)
|
|
23
|
+
subclass.instance_variable_set(:@config, subclass::Config.new(config.opts))
|
|
24
|
+
|
|
25
|
+
# Initialize attribute class
|
|
26
|
+
attribute_class = Class.new(self::Attribute)
|
|
27
|
+
attribute_class.jat_class = subclass
|
|
28
|
+
subclass.const_set(:Attribute, attribute_class)
|
|
29
|
+
|
|
30
|
+
# Initialize object presenter
|
|
31
|
+
presenter_class = Class.new(self::Presenter)
|
|
32
|
+
presenter_class.jat_class = subclass
|
|
33
|
+
subclass.const_set(:Presenter, presenter_class)
|
|
34
|
+
|
|
35
|
+
# Assign same attributes
|
|
36
|
+
attributes.each_value do |attribute|
|
|
37
|
+
params = attribute.params
|
|
38
|
+
subclass.attribute(params[:name], **params[:opts], ¶ms[:block])
|
|
39
|
+
end
|
|
21
40
|
|
|
22
41
|
super
|
|
23
42
|
end
|
|
24
43
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
def plugin(plugin, **opts)
|
|
45
|
+
if !plugin.is_a?(Symbol) && !plugin.is_a?(Module)
|
|
46
|
+
raise Error, "Plugin class must be a Symbol or a Module, #{plugin.inspect} given"
|
|
47
|
+
end
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
@config = config
|
|
31
|
-
end
|
|
49
|
+
plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
@attributes ||= Attributes.new
|
|
35
|
-
end
|
|
51
|
+
Plugins.before_load(plugin, self, **opts)
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@full_map ||= Map::Construct.new(self, :all).to_h
|
|
40
|
-
end
|
|
53
|
+
include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
|
|
54
|
+
extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
56
|
+
self::Attribute.include plugin::AttributeMethods if defined?(plugin::AttributeMethods)
|
|
57
|
+
self::Attribute.extend plugin::AttributeClassMethods if defined?(plugin::AttributeClassMethods)
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@exposed_map = nil
|
|
49
|
-
end
|
|
59
|
+
self::Config.include plugin::ConfigMethods if defined?(plugin::ConfigMethods)
|
|
60
|
+
self::Config.extend plugin::ConfigClassMethods if defined?(plugin::ConfigClassMethods)
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
clear
|
|
54
|
-
end
|
|
62
|
+
self::Presenter.include plugin::PresenterMethods if defined?(plugin::PresenterMethods)
|
|
63
|
+
self::Presenter.extend plugin::PresenterClassMethods if defined?(plugin::PresenterClassMethods)
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
config.copy_to(subclass)
|
|
59
|
-
attributes.copy_to(subclass)
|
|
65
|
+
Plugins.after_load(plugin, self, **opts)
|
|
66
|
+
plugin
|
|
60
67
|
end
|
|
61
|
-
end
|
|
62
68
|
|
|
63
|
-
module DSLClassMethods
|
|
64
69
|
def call
|
|
65
70
|
self
|
|
66
71
|
end
|
|
67
72
|
|
|
68
|
-
def to_h(object, context =
|
|
69
|
-
new
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def to_str(object, context = {})
|
|
73
|
-
new.to_str(object, context)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def type(new_type = nil)
|
|
77
|
-
return @type || raise(Error, "#{self} has no defined type") unless new_type
|
|
78
|
-
|
|
79
|
-
new_type = new_type.to_sym
|
|
80
|
-
define_method(:type) { new_type }
|
|
81
|
-
@type = new_type
|
|
73
|
+
def to_h(object, context = nil)
|
|
74
|
+
new(object, context || {}).to_h
|
|
82
75
|
end
|
|
83
76
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
raise Error, 'Key or block must be provided' if !key && !block
|
|
87
|
-
|
|
88
|
-
block ||= proc { |obj| obj.public_send(key) }
|
|
89
|
-
define_method(:id, &block)
|
|
77
|
+
def attributes
|
|
78
|
+
@attributes ||= {}
|
|
90
79
|
end
|
|
91
80
|
|
|
92
81
|
def attribute(name, **opts, &block)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
opts[:serializer] = serializer
|
|
98
|
-
add_attribute(name: name, opts: opts, block: block)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
def add_attribute(params)
|
|
104
|
-
opts = Opts.new(self, params)
|
|
105
|
-
|
|
106
|
-
Attribute.new(opts).tap do |attribute|
|
|
107
|
-
attributes << attribute
|
|
108
|
-
add_method(attribute)
|
|
109
|
-
clear
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def add_method(attribute)
|
|
114
|
-
block = attribute.block
|
|
115
|
-
return unless block
|
|
116
|
-
|
|
117
|
-
name = attribute.original_name
|
|
118
|
-
# Warning-free method redefinition
|
|
119
|
-
remove_method(name) if method_defined?(name)
|
|
120
|
-
define_method(name, &block)
|
|
82
|
+
new_attr = self::Attribute.new(name: name, opts: opts, block: block)
|
|
83
|
+
attributes[new_attr.name] = new_attr
|
|
84
|
+
self::Presenter.add_method(new_attr.original_name, new_attr.block)
|
|
85
|
+
new_attr
|
|
121
86
|
end
|
|
122
87
|
end
|
|
123
88
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
attr_reader :_context
|
|
127
|
-
|
|
128
|
-
def initialize(context = {}, full_map = nil)
|
|
129
|
-
@_context = context.dup
|
|
130
|
-
@_full_map = full_map
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def to_h(object, context = {})
|
|
134
|
-
_reinitialize(context)
|
|
135
|
-
|
|
136
|
-
Response.new(self, object).to_h
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def to_str(object, context = {})
|
|
140
|
-
_reinitialize(context)
|
|
141
|
-
Response.new(self, object).to_str
|
|
142
|
-
end
|
|
89
|
+
module InstanceMethods
|
|
90
|
+
attr_reader :object, :context
|
|
143
91
|
|
|
144
|
-
def
|
|
145
|
-
object
|
|
92
|
+
def initialize(object, context)
|
|
93
|
+
@object = object
|
|
94
|
+
@context = context
|
|
146
95
|
end
|
|
147
96
|
|
|
148
|
-
def
|
|
149
|
-
|
|
97
|
+
def to_h
|
|
98
|
+
raise Error, "Method #to_h must be implemented by plugin"
|
|
150
99
|
end
|
|
151
100
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def _full_map
|
|
157
|
-
@_full_map ||= begin
|
|
158
|
-
params = _context[:params]
|
|
159
|
-
fields = params && (params[:fields] || params['fields'])
|
|
160
|
-
includes = params && (params[:include] || params['include'])
|
|
161
|
-
Map.(self.class, fields, includes)
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def _map
|
|
166
|
-
@_map ||= _full_map.fetch(type)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
private
|
|
170
|
-
|
|
171
|
-
def _reinitialize(context)
|
|
172
|
-
new_params = context[:params]
|
|
173
|
-
old_params = _context[:params]
|
|
174
|
-
|
|
175
|
-
# maps depend on params, so we should clear them when params changed
|
|
176
|
-
if new_params != old_params
|
|
177
|
-
@_full_map = nil
|
|
178
|
-
@_map = nil
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
@_context = _context.merge!(context)
|
|
101
|
+
def config
|
|
102
|
+
self.class.config
|
|
182
103
|
end
|
|
183
104
|
end
|
|
184
105
|
|
|
185
106
|
extend ClassMethods
|
|
107
|
+
include InstanceMethods
|
|
186
108
|
end
|