snfoil-controller 1.0.0

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.
@@ -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: []