interactify 0.1.0.pre.alpha.1 → 0.3.0.pre.alpha.1

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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/interactor_wiring/error_context"
4
+
5
+ module Interactify
6
+ class InteractorWiring
7
+ class CallableRepresentation
8
+ attr_reader :filename, :klass, :wiring
9
+
10
+ delegate :interactor_lookup, to: :wiring
11
+
12
+ def initialize(filename:, klass:, wiring:)
13
+ @filename = filename
14
+ @klass = klass
15
+ @wiring = wiring
16
+ end
17
+
18
+ def validate_callable(error_context: ErrorContext.new)
19
+ if organizer?
20
+ assign_previously_defined(error_context:)
21
+ validate_children(error_context:)
22
+ end
23
+
24
+ validate_self(error_context:)
25
+ end
26
+
27
+ def expected_keys
28
+ klass.respond_to?(:expected_keys) ? Array(klass.expected_keys) : []
29
+ end
30
+
31
+ def promised_keys
32
+ klass.respond_to?(:promised_keys) ? Array(klass.promised_keys) : []
33
+ end
34
+
35
+ def all_keys
36
+ expected_keys.concat(promised_keys)
37
+ end
38
+
39
+ def inspect
40
+ "#<#{self.class.name}#{object_id} @filename=#{filename}, @klass=#{klass.name}>"
41
+ end
42
+
43
+ def organizer?
44
+ klass.respond_to?(:organized) && klass.organized.any?
45
+ end
46
+
47
+ def assign_previously_defined(error_context:)
48
+ return unless contract?
49
+
50
+ error_context.append_previously_defined_keys(all_keys)
51
+ end
52
+
53
+ def validate_children(error_context:)
54
+ klass.organized.each do |interactor|
55
+ interactor_as_callable = interactor_lookup[interactor]
56
+ next if interactor_as_callable.nil?
57
+
58
+ error_context = interactor_as_callable.validate_callable(error_context:)
59
+ end
60
+
61
+ error_context
62
+ end
63
+
64
+ private
65
+
66
+ def contract?
67
+ klass.ancestors.include? Interactor::Contracts
68
+ end
69
+
70
+ def validate_self(error_context:)
71
+ return error_context unless contract?
72
+
73
+ error_context.infer_missing_keys(self)
74
+ error_context.add_promised_keys(promised_keys)
75
+ error_context
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ class InteractorWiring
5
+ class Constants
6
+ attr_reader :root, :organizer_files, :interactor_files
7
+
8
+ def initialize(root:, organizer_files:, interactor_files:)
9
+ @root = root.is_a?(Pathname) ? root : Pathname.new(root)
10
+ @organizer_files = organizer_files
11
+ @interactor_files = interactor_files
12
+ end
13
+
14
+ def organizers
15
+ @organizers ||= organizer_files.flat_map do |f|
16
+ callables_in_file(f)
17
+ end.compact.select(&:organizer?)
18
+ end
19
+
20
+ def interactors
21
+ @interactors ||= interactor_files.flat_map do |f|
22
+ callables_in_file(f)
23
+ end.compact.reject(&:organizer?)
24
+ end
25
+
26
+ def interactor_lookup
27
+ @interactor_lookup ||= (interactors + organizers).index_by(&:klass)
28
+ end
29
+
30
+ private
31
+
32
+ def callables_in_file(filename)
33
+ @callables_in_file ||= {}
34
+
35
+ @callables_in_file[filename] ||= _callables_in_file(filename)
36
+ end
37
+
38
+ def _callables_in_file(filename)
39
+ constant = constant_for(filename)
40
+ return if constant == Interactify
41
+
42
+ internal_klasses = internal_constants_for(constant)
43
+
44
+ ([constant] + internal_klasses).map do |k|
45
+ new_callable(filename, k, self)
46
+ end
47
+ end
48
+
49
+ def internal_constants_for(constant)
50
+ constant
51
+ .constants
52
+ .map { |sym| constant_from_symbol(constant, sym) }
53
+ .select { |pk| interactor_klass?(pk) }
54
+ end
55
+
56
+ def constant_from_symbol(constant, symbol)
57
+ constant.module_eval do
58
+ symbol.to_s.constantize
59
+ rescue StandardError
60
+ begin
61
+ "#{constant.name}::#{symbol}".constantize
62
+ rescue StandardError
63
+ nil
64
+ end
65
+ end
66
+ end
67
+
68
+ def interactor_klass?(object)
69
+ return unless object.is_a?(Class) && object.ancestors.include?(Interactor)
70
+ return if object.is_a?(Sidekiq::Job)
71
+
72
+ true
73
+ end
74
+
75
+ def new_callable(filename, klass, wiring)
76
+ CallableRepresentation.new(filename:, klass:, wiring:)
77
+ end
78
+
79
+ def constant_for(filename)
80
+ require filename
81
+
82
+ underscored_klass_name = underscored_klass_name(filename)
83
+ underscored_klass_name = trim_rails_design_pattern_folder underscored_klass_name
84
+
85
+ klass_name = underscored_klass_name.classify
86
+
87
+ should_pluralize = filename[underscored_klass_name.pluralize]
88
+ klass_name = klass_name.pluralize if should_pluralize
89
+
90
+ Object.const_get(klass_name)
91
+ end
92
+
93
+ # Example:
94
+ # trim_rails_folder("app/interactors/namespace/sub_namespace/class_name.rb")
95
+ # => "namespace/sub_namespace/class_name.rb"
96
+ def trim_rails_design_pattern_folder(filename)
97
+ rails_folders.each do |folder|
98
+ regexable_folder = Regexp.quote("#{folder}/")
99
+ regex = /^#{regexable_folder}/
100
+
101
+ return filename.gsub(regex, "") if filename.match?(regex)
102
+ end
103
+
104
+ filename
105
+ end
106
+
107
+ def rails_folders = Dir.glob(root / "*").map { Pathname.new _1 }.select(&:directory?).map { |f| File.basename(f) }
108
+
109
+ # Example:
110
+ # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
111
+ # "/namespace/sub_namespace/class_name.rb"
112
+ # ['', 'namespace', 'sub_namespace', 'class_name.rb']
113
+ # ['namespace', 'sub_namespace', 'class_name.rb']
114
+ def underscored_klass_name(filename)
115
+ filename.to_s # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
116
+ .gsub(root.to_s, "") # "/namespace/sub_namespace/class_name.rb"
117
+ .gsub("/concerns", "") # concerns directory is ignored by Zeitwerk
118
+ .split("/") # "['', 'namespace', 'sub_namespace', 'class_name.rb']
119
+ .compact_blank # "['namespace', 'sub_namespace', 'class_name.rb']
120
+ .join("/") # 'namespace/sub_namespace/class_name.rb'
121
+ .gsub(/\.rb\z/, "") # 'namespace/sub_namespace/class_name'
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ class InteractorWiring
5
+ class ErrorContext
6
+ def previously_defined_keys
7
+ @previously_defined_keys ||= Set.new
8
+ end
9
+
10
+ def append_previously_defined_keys(keys)
11
+ keys.each do |key|
12
+ previously_defined_keys << key
13
+ end
14
+ end
15
+
16
+ def missing_keys
17
+ @missing_keys ||= {}
18
+ end
19
+
20
+ def add_promised_keys(promised_keys)
21
+ promised_keys.each do |key|
22
+ previously_defined_keys << key
23
+ end
24
+ end
25
+
26
+ def infer_missing_keys(callable)
27
+ new_keys = callable.expected_keys
28
+ not_in_previous_keys = new_keys.reject { |key| previously_defined_keys.include?(key) }
29
+
30
+ add_missing_keys(callable, not_in_previous_keys)
31
+ end
32
+
33
+ def add_missing_keys(callable, not_in_previous_keys)
34
+ return if not_in_previous_keys.empty?
35
+
36
+ missing_keys[callable] ||= Set.new
37
+ missing_keys[callable] += not_in_previous_keys
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ class InteractorWiring
5
+ class Files
6
+ attr_reader :root
7
+
8
+ def initialize(root:)
9
+ @root = root
10
+ end
11
+
12
+ def organizer_files
13
+ possible_files.select { |_, contents| organizer_file?(contents) }.map(&:first).sort
14
+ end
15
+
16
+ def interactor_files
17
+ possible_files.select { |_, contents| interactor_file?(contents) }.map(&:first).sort
18
+ end
19
+
20
+ private
21
+
22
+ def organizer_file?(code)
23
+ organizer?(code) && (interactified?(code) || vanilla_organizer?(code))
24
+ end
25
+
26
+ def interactor_file?(code)
27
+ !organizer?(code) && (interactified?(code) || vanilla_interactor?(code))
28
+ end
29
+
30
+ def vanilla_organizer?(code)
31
+ code[/include Interactor::Organizer/]
32
+ end
33
+
34
+ def vanilla_interactor?(code)
35
+ code[/include Interactor$/]
36
+ end
37
+
38
+ def interactified?(code)
39
+ code["include Interactify"]
40
+ end
41
+
42
+ def organizer?(code)
43
+ code[/^\s+organize/]
44
+ end
45
+
46
+ def possible_files
47
+ @possible_files ||= Dir.glob("#{root}/**/*.rb").map { |f| [f, File.read(f)] }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,305 +1,90 @@
1
- require 'active_support/all'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+
5
+ require "interactify/interactor_wiring/callable_representation"
6
+ require "interactify/interactor_wiring/constants"
7
+ require "interactify/interactor_wiring/files"
2
8
 
3
9
  module Interactify
4
10
  class InteractorWiring
5
- attr_reader :root, :namespace, :ignore
11
+ attr_reader :root, :ignore
6
12
 
7
- def initialize(root: Rails.root, namespace: 'Object', ignore: [])
8
- @root = root.to_s.gsub(%r{/$}, '')
9
- @namespace = namespace
13
+ def initialize(root: Rails.root, ignore: [])
14
+ @root = root.to_s.gsub(%r{/$}, "")
10
15
  @ignore = ignore
11
16
  end
12
17
 
13
- concerning :Validation do
14
- def validate_app
15
- errors = organizers.each_with_object({}) do |organizer, all_errors|
16
- next if ignore_klass?(ignore, organizer.klass)
17
-
18
- errors = organizer.validate_callable
19
- all_errors[organizer] = errors
20
- end
18
+ def validate_app
19
+ errors = organizers.each_with_object({}) do |organizer, all_errors|
20
+ next if ignore_klass?(ignore, organizer.klass)
21
21
 
22
- format_errors(errors)
22
+ errors = organizer.validate_callable
23
+ all_errors[organizer] = errors
23
24
  end
24
25
 
25
- def format_errors(all_errors)
26
- formatted_errors = []
27
-
28
- all_errors.each do |organizer, error_context|
29
- next if ignore_klass?(ignore, organizer.klass)
30
-
31
- error_context.missing_keys.each do |interactor, missing|
32
- next if ignore_klass?(ignore, interactor.klass)
33
-
34
- formatted_errors << <<~ERROR
35
- Missing keys: #{missing.to_a.map(&:to_sym).map(&:inspect).join(', ')}
36
- in: #{interactor.klass}
37
- for: #{organizer.klass}
38
- ERROR
39
- end
40
- end
41
-
42
- formatted_errors.join("\n\n")
43
- end
44
-
45
- def ignore_klass?(ignore, klass)
46
- case ignore
47
- when Array
48
- ignore.any? { ignore_klass?(_1, klass) }
49
- when Regexp
50
- klass.to_s =~ ignore
51
- when String
52
- klass.to_s[ignore]
53
- when Proc
54
- ignore.call(klass)
55
- when Class
56
- klass <= ignore
57
- end
58
- end
26
+ format_errors(errors)
59
27
  end
60
28
 
61
- class CallableRepresentation
62
- attr_reader :filename, :klass, :wiring
29
+ def format_errors(all_errors)
30
+ formatted_errors = []
63
31
 
64
- delegate :interactor_lookup, to: :wiring
65
-
66
- def initialize(filename:, klass:, wiring:, organizer: nil)
67
- @filename = filename
68
- @klass = klass
69
- @wiring = wiring
70
- end
71
-
72
- def validate_callable(error_context: ErrorContext.new)
73
- if organizer?
74
- assign_previously_defined(error_context: error_context)
75
- validate_children(error_context: error_context)
76
- end
77
-
78
- validate_self(error_context: error_context)
79
- end
80
-
81
- def promised_keys
82
- Array(klass.contract.promises.instance_eval { @terms }.json&.rules&.keys)
83
- end
84
-
85
- def expected_keys
86
- Array(klass.contract.expectations.instance_eval { @terms }.json&.rules&.keys)
32
+ each_error(all_errors) do |missing, interactor, organizer|
33
+ format_error(missing, interactor, organizer, formatted_errors)
87
34
  end
88
35
 
89
- def all_keys
90
- expected_keys.concat(promised_keys)
91
- end
92
-
93
- def inspect
94
- "#<#{self.class.name}#{object_id} @filename=#{filename}, @klass=#{klass.name}>"
95
- end
96
-
97
- def organizer?
98
- klass.respond_to?(:organized)
99
- end
100
-
101
- def assign_previously_defined(error_context:)
102
- return unless contract?
103
-
104
- error_context.append_previously_defined_keys(all_keys)
105
- end
106
-
107
- def validate_children(error_context:)
108
- klass.organized.each do |interactor|
109
- nested_callable = interactor_lookup[interactor]
110
- next if nested_callable.nil?
111
-
112
- error_context = nested_callable.validate_callable(error_context: error_context)
113
- end
114
-
115
- error_context
116
- end
117
-
118
- private
119
-
120
- def contract?
121
- klass.ancestors.include? Interactor::Contracts
122
- end
123
-
124
- def validate_self(error_context:)
125
- return error_context unless contract?
126
-
127
- error_context.infer_missing_keys(self)
128
- error_context.add_promised_keys(promised_keys)
129
- error_context
130
- end
36
+ formatted_errors.join("\n\n")
131
37
  end
132
38
 
133
- class ErrorContext
134
- def previously_defined_keys
135
- @previously_defined_keys ||= Set.new
136
- end
137
-
138
- def append_previously_defined_keys(keys)
139
- keys.each do |key|
140
- previously_defined_keys << key
141
- end
142
- end
39
+ def each_error(all_errors)
40
+ all_errors.each do |organizer, error_context|
41
+ next if ignore_klass?(ignore, organizer.klass)
143
42
 
144
- def missing_keys
145
- @missing_keys ||= {}
146
- end
43
+ error_context.missing_keys.each do |interactor, missing|
44
+ next if ignore_klass?(ignore, interactor.klass)
147
45
 
148
- def add_promised_keys(promised_keys)
149
- promised_keys.each do |key|
150
- previously_defined_keys << key
46
+ yield missing, interactor, organizer
151
47
  end
152
48
  end
153
-
154
- def infer_missing_keys(callable)
155
- new_keys = callable.expected_keys
156
- not_in_previous_keys = new_keys.reject { |key| previously_defined_keys.include?(key) }
157
-
158
- add_missing_keys(callable, not_in_previous_keys)
159
- end
160
-
161
- def add_missing_keys(callable, not_in_previous_keys)
162
- return if not_in_previous_keys.empty?
163
-
164
- missing_keys[callable] ||= Set.new
165
- missing_keys[callable] += not_in_previous_keys
166
- end
167
49
  end
168
50
 
169
- concerning :Constants do
170
- def organizers
171
- @organizers ||= organizer_files.flat_map do |f|
172
- next if f[/interactor_organizer_contracts/] || f[/interactor_contracts/]
173
-
174
- callables_in_file(f)
175
- end.compact.select(&:organizer?)
176
- end
177
-
178
- def interactors
179
- @interactors ||= interactor_files.flat_map do |f|
180
- callables_in_file(f)
181
- end.compact.reject(&:organizer?)
182
- end
183
-
184
- def callables_in_file(f)
185
- @callables_in_file ||= {}
186
-
187
- @callables_in_file[f] ||= _callables_in_file(f)
188
- end
189
-
190
- def _callables_in_file(f)
191
- constant = constant_for(f)
192
- return if constant == Interactify
193
-
194
- internal_klasses = internal_constants_for(constant)
195
-
196
- ([constant] + internal_klasses).map { |k| new_callable(f, k, self) }
197
- end
198
-
199
- def internal_constants_for(constant)
200
- constant
201
- .constants
202
- .map { |sym| constant_from_symbol(constant, sym) }
203
- .select { |pk| interactor_klass?(pk) }
204
- end
205
-
206
- def constant_from_symbol(constant, symbol)
207
- constant.module_eval do
208
- symbol.to_s.constantize
209
- rescue StandardError
210
- "#{constant.name}::#{symbol}".constantize rescue nil
211
- end
212
- end
213
-
214
- def interactor_klass?(object)
215
- return unless object.is_a? Class
216
-
217
- object.ancestors.include?(Interactor) || object.ancestors.include?(Interactor::Organizer)
218
- end
219
-
220
- def new_callable(filename, klass, wiring)
221
- CallableRepresentation.new(filename: filename, klass: klass, wiring: wiring)
222
- end
223
-
224
- def interactor_lookup
225
- @interactor_lookup ||= (interactors + organizers).index_by(&:klass)
226
- end
227
-
228
- private
229
-
230
- def constant_for(filename)
231
- require filename
232
-
233
- underscored_klass_name = underscored_klass_name_without_outer_namespace(filename)
234
- underscored_klass_name = trim_rails_folder underscored_klass_name
235
-
236
- klass_name = underscored_klass_name.classify
237
-
238
- should_pluralize = filename[underscored_klass_name.pluralize]
239
- klass_name = klass_name.pluralize if should_pluralize
240
-
241
- outer_namespace_constant.const_get(klass_name)
242
- end
243
-
244
- # Example:
245
- # trim_rails_folder("interactors/namespace/sub_namespace/class_name.rb")
246
- # => "namespace/sub_namespace/class_name.rb"
247
- def trim_rails_folder(filename)
248
- rails_folders = Dir.glob(Interactify.root / '*').map { |f| File.basename(f) }
249
-
250
- rails_folders.each do |folder|
251
- regexable_folder = Regexp.quote("#{folder}/")
252
- regex = /^#{regexable_folder}/
253
-
254
- return filename.gsub(regex, '') if filename.match?(regex)
255
- end
256
-
257
- filename
258
- end
259
-
260
- # Example:
261
- # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
262
- # "/namespace/sub_namespace/class_name.rb"
263
- # ['', 'namespace', 'sub_namespace', 'class_name.rb']
264
- # ['namespace', 'sub_namespace', 'class_name.rb']
265
- # remove outernamespace (SpecSupport)
266
- def underscored_klass_name_without_outer_namespace(filename)
267
- filename.to_s # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
268
- .gsub(root.to_s, '') # "/namespace/sub_namespace/class_name.rb"
269
- .gsub('/concerns', '') # concerns directory is ignored by Zeitwerk
270
- .split('/') # "['', 'namespace', 'sub_namespace', 'class_name.rb']
271
- .compact_blank # "['namespace', 'sub_namespace', 'class_name.rb']
272
- .reject.with_index { |segment, i| i.zero? && segment == namespace }
273
- .join('/') # 'namespace/sub_namespace/class_name.rb'
274
- .gsub(/\.rb\z/, '') # 'namespace/sub_namespace/class_name'
275
- end
276
-
277
- def outer_namespace_constant
278
- @outer_namespace_constant ||= Object.const_get(namespace)
279
- end
51
+ def format_error(missing, interactor, organizer, formatted_errors)
52
+ formatted_errors << <<~ERROR
53
+ Missing keys: #{missing.to_a.map(&:to_sym).map(&:inspect).join(', ')}
54
+ expected in: #{interactor.klass}
55
+ called by: #{organizer.klass}
56
+ ERROR
280
57
  end
281
58
 
282
- concerning :Files do
283
- def organizer_files
284
- possible_files.select { |_, contents| organizer_file?(contents) }.map(&:first).sort
59
+ def ignore_klass?(ignore, klass)
60
+ case ignore
61
+ when Array
62
+ ignore.any? { ignore_klass?(_1, klass) }
63
+ when Regexp
64
+ klass.to_s =~ ignore
65
+ when String
66
+ klass.to_s[ignore]
67
+ when Proc
68
+ ignore.call(klass)
69
+ when Class
70
+ klass <= ignore
285
71
  end
72
+ end
286
73
 
287
- def interactor_files
288
- possible_files.select { |_, contents| interactor_file?(contents) }.map(&:first).sort
289
- end
74
+ delegate :organizers, :interactors, :interactor_lookup, to: :constants
290
75
 
291
- def organizer_file?(file_contents)
292
- (file_contents['include Interactify'] || file_contents[/include Interactor::Organizer/]) &&
293
- file_contents[/^\s+organize/]
294
- end
76
+ def constants
77
+ @constants ||= Constants.new(
78
+ root:,
79
+ organizer_files:,
80
+ interactor_files:
81
+ )
82
+ end
295
83
 
296
- def interactor_file?(file_contents)
297
- file_contents['include Interactify'] || file_contents[/include Interactor$/]
298
- end
84
+ delegate :organizer_files, :interactor_files, to: :files
299
85
 
300
- def possible_files
301
- @possible_files ||= Dir.glob("#{root}/**/*.rb").map { |f| [f, File.read(f)] }
302
- end
86
+ def files
87
+ @files ||= Files.new(root:)
303
88
  end
304
89
  end
305
90
  end