snfoil-controller 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Matthew Howes
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'active_support/concern'
18
+ require 'active_support/core_ext/string/inflections'
19
+
20
+ module SnFoil
21
+ module Deserializer
22
+ # ActiveSupport::Concern for base deserializer functionality
23
+ # Add the following class methods:
24
+ # * attribute
25
+ # * attributes
26
+ # * belongs_to
27
+ # * has_many
28
+ # * has_one
29
+ # * key_transform
30
+ #
31
+ # @author Matthew Howes
32
+ #
33
+ # @since 0.1.0
34
+ module Base
35
+ extend ActiveSupport::Concern
36
+
37
+ class_methods do
38
+ attr_reader :snfoil_attribute_transforms,
39
+ :snfoil_key_transform
40
+
41
+ def key_transform(transform = nil, &block)
42
+ @snfoil_key_transform = transform || block
43
+ end
44
+
45
+ def attribute(key, **options, &block)
46
+ (@snfoil_attribute_transforms ||= {})[key] = options.merge(transform_type: :attribute, block: block)
47
+ end
48
+
49
+ def attributes(*fields, **options, &block)
50
+ fields.each { |field| attribute(field, **options, &block) }
51
+ end
52
+
53
+ def belongs_to(key, deserializer:, **options, &block)
54
+ (@snfoil_attribute_transforms ||= {})[key] = options.merge(deserializer: deserializer, transform_type: :has_one, block: block)
55
+ end
56
+ alias_method :has_one, :belongs_to
57
+
58
+ def has_many(key, deserializer:, **options, &block) # rubocop:disable Naming/PredicateName
59
+ (@snfoil_attribute_transforms ||= {})[key] = options.merge(deserializer: deserializer, transform_type: :has_many, block: block)
60
+ end
61
+
62
+ def inherited(subclass)
63
+ super
64
+
65
+ instance_variables.grep(/@snfoil_.+/).each do |i|
66
+ subclass.instance_variable_set(i, instance_variable_get(i).dup)
67
+ end
68
+ end
69
+ end
70
+
71
+ included do
72
+ attr_reader :data, :config
73
+
74
+ def initialize(input, **config)
75
+ @data = normalize_keys(input)
76
+ @config = config
77
+ end
78
+
79
+ def parse
80
+ raise '#parse not implemented'
81
+ end
82
+
83
+ def to_hash
84
+ parse
85
+ end
86
+
87
+ alias_method :to_hash, :parse
88
+ alias_method :to_h, :parse
89
+ end
90
+
91
+ protected
92
+
93
+ def normalize_keys(input)
94
+ input = input.transform_keys do |key|
95
+ apply_key_normalize(key)
96
+ end
97
+
98
+ input.transform_values do |value|
99
+ check_deep_keys(value)
100
+ end
101
+ end
102
+
103
+ def check_deep_keys(value)
104
+ case value
105
+ when Hash
106
+ normalize_keys(value)
107
+ when Array
108
+ value.map { |v| check_deep_keys(v) }
109
+ else
110
+ value
111
+ end
112
+ end
113
+
114
+ def apply_key_normalize(key)
115
+ @snfoil_key_transform ||= self.class.snfoil_key_transform ||
116
+ :underscore
117
+ case @snfoil_key_transform
118
+ when Symbol
119
+ key.to_s.send(@snfoil_key_transform)
120
+ when Proc
121
+ @snfoil_key_transform.call(key.to_s)
122
+ else
123
+ key
124
+ end.to_sym
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Matthew Howes
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'active_support/concern'
18
+ require_relative 'base'
19
+
20
+ module SnFoil
21
+ module Deserializer
22
+ # ActiveSupport::Concern for JSON deserializer functionality
23
+ # Initialize class with json payload and call #parse to output normalized hash
24
+ #
25
+ # @author Matthew Howes
26
+ #
27
+ # @since 0.1.0
28
+ module JSON
29
+ extend ActiveSupport::Concern
30
+
31
+ included do # rubocop:disable Metrics/BlockLength reason: These methods need to be in included to be overridable
32
+ include SnFoil::Deserializer::Base
33
+
34
+ def parse
35
+ apply_transforms({}, data)
36
+ end
37
+
38
+ protected
39
+
40
+ def apply_transforms(output, input)
41
+ (self.class.snfoil_attribute_transforms || {}).reduce(output) do |transformed_output, transform|
42
+ apply_transform(transformed_output, input, transform[0], **transform[1])
43
+ end
44
+ end
45
+
46
+ def apply_transform(output, input, key, **options)
47
+ case options[:transform_type]
48
+ when :attribute
49
+ parse_attribute_transform(output, input, key, **options)
50
+ when :has_one
51
+ parse_has_one_transform(output, input, key, **options)
52
+ when :has_many
53
+ parse_has_many_transform(output, input, key, **options)
54
+ end
55
+ end
56
+
57
+ def parse_attribute_transform(output, input, key, **options)
58
+ value = find_attribute(input, key, **options)
59
+ return output unless value
60
+
61
+ output.merge key => value
62
+ end
63
+
64
+ def parse_has_one_transform(output, input, key, deserializer:, **options)
65
+ resource_data = find_attribute(input, key, **options)
66
+ return output unless resource_data
67
+
68
+ output[key] = deserializer.new(resource_data, **options).parse
69
+ output
70
+ end
71
+
72
+ def parse_has_many_transform(output, input, key, deserializer:, **options)
73
+ array_data = find_attribute(input, key, **options)
74
+ return output unless array_data
75
+
76
+ array_data = [array_data] if array_data.is_a? Hash
77
+ output[key] = array_data.map { |r| deserializer.new(r, **options).parse }
78
+ output
79
+ end
80
+
81
+ def find_attribute(input, key, block: nil, with: nil, **options)
82
+ return unless input
83
+
84
+ if block
85
+ instance_exec(input, key, **options, &block)
86
+ elsif with
87
+ send(with, input, key, **options)
88
+ else
89
+ value_key = options.fetch(:key) { key }
90
+ value_key = "#{options[:prefix]}#{key}".to_sym if options[:prefix]
91
+
92
+ input[value_key.to_sym]
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Matthew Howes
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'active_support/concern'
18
+ require_relative 'json'
19
+
20
+ module SnFoil
21
+ module Deserializer
22
+ # ActiveSupport::Concern for JSONAPI deserializer functionality
23
+ # Initialize class with jsonapi payload and call #parse to output normalized hash
24
+ #
25
+ # @author Matthew Howes
26
+ #
27
+ # @since 0.1.0
28
+ module JSONAPI
29
+ extend ActiveSupport::Concern
30
+
31
+ included do # rubocop:disable Metrics/BlockLength reason: These methods need to be in included to be overridable
32
+ include SnFoil::Deserializer::JSON
33
+
34
+ module_eval do
35
+ def parse
36
+ input = data[:data] || data
37
+ return apply_transforms(data_id({}, input), input) unless input.is_a? Array
38
+
39
+ input.map { |d| apply_transforms(data_id({}, d), d) }
40
+ end
41
+ end
42
+
43
+ def included
44
+ @included ||= config[:included] || data[:included]
45
+ end
46
+
47
+ private
48
+
49
+ def data_id(output, data)
50
+ if data[:id]
51
+ output[:id] = data[:id]
52
+ elsif data[:'local:id']
53
+ output[:'local:id'] = data[:'local:id']
54
+ end
55
+
56
+ output
57
+ end
58
+
59
+ def parse_attribute_transform(output, input, key, **options)
60
+ value = find_attribute(input[:attributes], key, **options)
61
+ return output unless value
62
+
63
+ output.merge key => value
64
+ end
65
+
66
+ def parse_has_one_transform(output, input, key, deserializer:, **options)
67
+ resource_data = find_relationship(input, key, **options)
68
+ return output unless resource_data
69
+
70
+ output[key] = deserializer.new(resource_data, **options, included: included).parse
71
+ output
72
+ end
73
+
74
+ def parse_has_many_transform(output, input, key, deserializer:, **options)
75
+ resource_data = find_relationships(input, key, **options)
76
+ return output unless resource_data
77
+
78
+ output[key] = resource_data.map { |r| deserializer.new(r, **options, included: included).parse }
79
+ output
80
+ end
81
+
82
+ def find_relationships(input, key, **options)
83
+ array_data = find_attribute(input[:relationships], key, **options)
84
+ return unless array_data
85
+
86
+ array_data = array_data[:data]
87
+ array_data = [array_data] unless array_data.is_a? Array
88
+ array_data.map do |resource_data|
89
+ lookup_relationship(**resource_data) || resource_data
90
+ end
91
+ end
92
+
93
+ def find_relationship(input, key, **options)
94
+ resource_data = find_attribute(input[:relationships], key, **options)
95
+ return unless resource_data
96
+
97
+ lookup_relationship(**resource_data[:data]) || resource_data
98
+ end
99
+
100
+ def lookup_relationship(type:, **options)
101
+ id = options[:id]
102
+ lid = options[:'local:id']
103
+
104
+ included&.find do |x|
105
+ x[:type].eql?(type) &&
106
+ if id
107
+ x[:id].eql?(id)
108
+ elsif lid
109
+ x[:'local:id'].eql?(lid)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/snfoil/controller/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'snfoil-controller'
7
+ spec.version = SnFoil::Controller::VERSION
8
+ spec.authors = ['Matthew Howes', 'Cliff Campbell']
9
+ spec.email = ['howeszy@gmail.com', 'cliffcampbell@hey.com']
10
+
11
+ spec.summary = 'Seperate Display Logic from Business Logic'
12
+ spec.description = 'A context-like experience for your controllers'
13
+ spec.homepage = 'https://github.com/limited-effort/snfoil-controller'
14
+ spec.license = 'Apache-2.0'
15
+ spec.required_ruby_version = '>= 2.7'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = 'https://github.com/limited-effort/snfoil-controller/blob/main/CHANGELOG.md'
20
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ ignore_list = %r{\A(?:test/|spec/|bin/|features/|Rakefile|\.\w)}
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) { `git ls-files -z`.split("\x0").reject { |f| f.match(ignore_list) } }
26
+
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'activesupport', '>= 5.2.6'
32
+ spec.add_dependency 'jsonapi-serializer', '~> 2.2'
33
+ spec.add_dependency 'snfoil-context', '~> 1.0'
34
+
35
+ spec.add_development_dependency 'bundle-audit', '~> 0.1.0'
36
+ spec.add_development_dependency 'fasterer', '~> 0.10.0'
37
+ spec.add_development_dependency 'oj'
38
+ spec.add_development_dependency 'pry-byebug', '~> 3.9'
39
+ spec.add_development_dependency 'rake', '~> 13.0'
40
+ spec.add_development_dependency 'rspec', '~> 3.10'
41
+ spec.add_development_dependency 'rubocop', '1.29'
42
+ spec.add_development_dependency 'rubocop-performance', '~> 1.11'
43
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.5'
44
+ end
metadata ADDED
@@ -0,0 +1,228 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snfoil-controller
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Howes
8
+ - Cliff Campbell
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2022-05-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 5.2.6
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 5.2.6
28
+ - !ruby/object:Gem::Dependency
29
+ name: jsonapi-serializer
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.2'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.2'
42
+ - !ruby/object:Gem::Dependency
43
+ name: snfoil-context
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundle-audit
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 0.1.0
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 0.1.0
70
+ - !ruby/object:Gem::Dependency
71
+ name: fasterer
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 0.10.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 0.10.0
84
+ - !ruby/object:Gem::Dependency
85
+ name: oj
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: pry-byebug
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '3.9'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '3.9'
112
+ - !ruby/object:Gem::Dependency
113
+ name: rake
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '13.0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '13.0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rspec
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '3.10'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '3.10'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rubocop
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - '='
145
+ - !ruby/object:Gem::Version
146
+ version: '1.29'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '='
152
+ - !ruby/object:Gem::Version
153
+ version: '1.29'
154
+ - !ruby/object:Gem::Dependency
155
+ name: rubocop-performance
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '1.11'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '1.11'
168
+ - !ruby/object:Gem::Dependency
169
+ name: rubocop-rspec
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '2.5'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - "~>"
180
+ - !ruby/object:Gem::Version
181
+ version: '2.5'
182
+ description: A context-like experience for your controllers
183
+ email:
184
+ - howeszy@gmail.com
185
+ - cliffcampbell@hey.com
186
+ executables: []
187
+ extensions: []
188
+ extra_rdoc_files: []
189
+ files:
190
+ - CHANGELOG.md
191
+ - CODE_OF_CONDUCT.md
192
+ - Gemfile
193
+ - LICENSE.txt
194
+ - README.md
195
+ - lib/snfoil/controller.rb
196
+ - lib/snfoil/controller/version.rb
197
+ - lib/snfoil/deserializer/base.rb
198
+ - lib/snfoil/deserializer/json.rb
199
+ - lib/snfoil/deserializer/jsonapi.rb
200
+ - snfoil-controller.gemspec
201
+ homepage: https://github.com/limited-effort/snfoil-controller
202
+ licenses:
203
+ - Apache-2.0
204
+ metadata:
205
+ homepage_uri: https://github.com/limited-effort/snfoil-controller
206
+ source_code_uri: https://github.com/limited-effort/snfoil-controller
207
+ changelog_uri: https://github.com/limited-effort/snfoil-controller/blob/main/CHANGELOG.md
208
+ rubygems_mfa_required: 'true'
209
+ post_install_message:
210
+ rdoc_options: []
211
+ require_paths:
212
+ - lib
213
+ required_ruby_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '2.7'
218
+ required_rubygems_version: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ requirements: []
224
+ rubygems_version: 3.1.6
225
+ signing_key:
226
+ specification_version: 4
227
+ summary: Seperate Display Logic from Business Logic
228
+ test_files: []