scarpe-components 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,454 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scarpe; end
4
+ module Scarpe::Components; end
5
+ module Scarpe
6
+ # Scarpe::Promise is a promises library, but one with no form of built-in
7
+ # concurrency. Instead, promise callbacks are executed synchronously.
8
+ # Even execution is usually synchronous, but can also be handled manually
9
+ # for forms of execution not controlled in Ruby (like Webview.)
10
+ #
11
+ # Funny thing... We need promises as an API concept since we have a JS event
12
+ # loop doing its thing, and we need to respond to actions that it takes.
13
+ # But there's not really a Ruby implementation of Promises *without* an
14
+ # attached form of concurrency. So here we are, writing our own :-/
15
+ #
16
+ # In theory you could probably write some kind of "no-op thread pool"
17
+ # for the ruby-concurrency gem, pass it manually to every promise we
18
+ # created and then raise an exception any time we tried to do something
19
+ # in the background. That's probably more code than writing our own, though,
20
+ # and we'd be fighting it constantly.
21
+ #
22
+ # This class is inspired by concurrent-ruby [Promise](https://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Promise.html)
23
+ # which is inspired by Javascript Promises, which is what we actually need
24
+ # for our use case. We can't easily tell when our WebView begins processing
25
+ # our request, which removes the :processing state. This can be used for
26
+ # executing JS, but also generally waiting on events.
27
+ #
28
+ # We don't fully control ordering here, so it *is* conceivable that a
29
+ # child waiting on a parent can be randomly fulfilled, even if we didn't
30
+ # expect it. We don't consider that an error. Similarly, we'll call
31
+ # on_scheduled callbacks if a promise is fulfilled, even though we
32
+ # never explicitly scheduled it. If a promise is *rejected* without
33
+ # ever being scheduled, we won't call those callbacks.
34
+ class Promise
35
+ include Shoes::Log
36
+
37
+ # The unscheduled promise state means it's waiting on a parent promise that
38
+ # hasn't completed yet. The pending state means it's waiting to execute.
39
+ # Fulfilled means it has completed successfully and returned a value,
40
+ # while rejected means it has failed, normally producing a reason.
41
+ PROMISE_STATES = [:unscheduled, :pending, :fulfilled, :rejected]
42
+
43
+ # The state of the promise, which should be one of PROMISE_STATES
44
+ attr_reader :state
45
+
46
+ # The parent promises of this promise, sometimes an empty array
47
+ attr_reader :parents
48
+
49
+ # If the promise is fulfilled, this is the value returned
50
+ attr_reader :returned_value
51
+
52
+ # If the promise is rejected, this is the reason, sometimes an exception
53
+ attr_reader :reason
54
+
55
+ # Create a promise and then instantly fulfill it.
56
+ def self.fulfilled(return_val = nil, parents: [], &block)
57
+ p = Promise.new(parents: parents, &block)
58
+ p.fulfilled!(return_val)
59
+ p
60
+ end
61
+
62
+ # Create a promise and then instantly reject it.
63
+ def self.rejected(reason = nil, parents: [])
64
+ p = Promise.new(parents: parents)
65
+ p.rejected!(reason)
66
+ p
67
+ end
68
+
69
+ # Fulfill the promise, setting the returned_value to value
70
+ def fulfilled!(value = nil)
71
+ set_state(:fulfilled, value)
72
+ end
73
+
74
+ # Reject the promise, setting the reason to reason
75
+ def rejected!(reason = nil)
76
+ set_state(:rejected, reason)
77
+ end
78
+
79
+ # Create a new promise with this promise as a parent. It runs the
80
+ # specified code in block when scheduled.
81
+ def then(&block)
82
+ Promise.new(parents: [self], &block)
83
+ end
84
+
85
+ # The Promise.new method, along with all the various handlers,
86
+ # are pretty raw. They'll do what promises do, but they're not
87
+ # the prettiest. However, they ensure that guarantees are made
88
+ # and so on, so they're great as plumbing under the syntactic
89
+ # sugar above.
90
+ #
91
+ # Note that the state passed in may not be the actual initial
92
+ # state. If a parent is rejected, the state will become
93
+ # rejected. If no parents are waiting or failed then a state
94
+ # of nil or :unscheduled will become :pending.
95
+ #
96
+ # @param state [Symbol] One of PROMISE_STATES for the initial state
97
+ # @param parents [Array] A list of promises that must be fulfilled before this one is scheduled
98
+ # @yield A block that executes when this promise is scheduled - when its parents, if any, are all fulfilled
99
+ def initialize(state: nil, parents: [], &scheduler)
100
+ log_init("Promise")
101
+
102
+ # These are as initially specified, and effectively immutable
103
+ @state = state
104
+ @parents = parents
105
+
106
+ # These are what we're waiting on, and will be
107
+ # removed as time goes forward.
108
+ @waiting_on = parents.select { |p| !p.complete? }
109
+ @on_fulfilled = []
110
+ @on_rejected = []
111
+ @on_scheduled = []
112
+ @scheduler = scheduler
113
+ @executor = nil
114
+ @returned_value = nil
115
+ @reason = nil
116
+
117
+ if complete?
118
+ # Did we start out already fulfilled or rejected?
119
+ # If so, we can skip a lot of fiddly checking.
120
+ # Don't need a scheduler or to care about parents
121
+ # or anything.
122
+ @waiting_on = []
123
+ @scheduler = nil
124
+ elsif @parents.any? { |p| p.state == :rejected }
125
+ @state = :rejected
126
+ @waiting_on = []
127
+ @scheduler = nil
128
+ elsif @state == :pending
129
+ # Did we get an explicit :pending? Then we don't need
130
+ # to schedule ourselves, or care about the scheduler
131
+ # in general.
132
+ @scheduler = nil
133
+ elsif @state.nil? || @state == :unscheduled
134
+ # If no state was given or we're unscheduled, we'll
135
+ # wait until our parents have all completed to
136
+ # schedule ourselves.
137
+
138
+ if @waiting_on.empty?
139
+ # No further dependencies, we can schedule ourselves
140
+ @state = :pending
141
+
142
+ # We have no on_scheduled handlers yet, but this will
143
+ # call and clear the scheduler.
144
+ call_handlers_for(:pending)
145
+ else
146
+ # We're still waiting on somebody, no scheduling yet
147
+ @state = :unscheduled
148
+ @waiting_on.each do |dep|
149
+ dep.on_fulfilled { parent_fulfilled!(dep) }
150
+ dep.on_rejected { parent_rejected!(dep) }
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ # Return true if the Promise is either fulfilled or rejected.
157
+ #
158
+ # @return [Boolean] true if the promise is fulfilled or rejected
159
+ def complete?
160
+ @state == :fulfilled || @state == :rejected
161
+ end
162
+
163
+ # Return true if the promise is already fulfilled.
164
+ #
165
+ # @return [Boolean] true if the promise is fulfilled
166
+ def fulfilled?
167
+ @state == :fulfilled
168
+ end
169
+
170
+ # Return true if the promise is already rejected.
171
+ #
172
+ # @return [Boolean] true if the promise is rejected
173
+ def rejected?
174
+ @state == :rejected
175
+ end
176
+
177
+ # An inspect method to give slightly smaller output, for ease of reading in irb
178
+ def inspect
179
+ "#<Scarpe::Promise:#{object_id} " +
180
+ "@state=#{@state.inspect} @parents=#{@parents.inspect} " +
181
+ "@waiting_on=#{@waiting_on.inspect} @on_fulfilled=#{@on_fulfilled.size} " +
182
+ "@on_rejected=#{@on_rejected.size} @on_scheduled=#{@on_scheduled.size} " +
183
+ "@scheduler=#{@scheduler ? "Y" : "N"} @executor=#{@executor ? "Y" : "N"} " +
184
+ "@returned_value=#{@returned_value.inspect} @reason=#{@reason.inspect}" +
185
+ ">"
186
+ end
187
+
188
+ # These promises are mostly designed for external execution.
189
+ # You could put together your own thread-pool, or use RPC,
190
+ # a WebView, a database or similar source of external calculation.
191
+ # But in many cases it's reasonable to execute locally.
192
+ # In those cases, you can register an executor which will be
193
+ # called when the promise is ready to execute but has not yet
194
+ # done so. Registering an executor on a promise that is
195
+ # already fulfilled is an error. Registering an executor on
196
+ # a promise that has already rejected is a no-op.
197
+ def to_execute(&block)
198
+ case @state
199
+ when :fulfilled
200
+ # Should this be a no-op instead?
201
+ raise Scarpe::NoOperationError, "Registering an executor on an already fulfilled promise means it will never run!"
202
+ when :rejected
203
+ return
204
+ when :unscheduled
205
+ @executor = block # save for later
206
+ when :pending
207
+ @executor = block
208
+ call_executor
209
+ else
210
+ raise Scarpe::InternalError, "Internal error, illegal state!"
211
+ end
212
+
213
+ self
214
+ end
215
+
216
+ private
217
+
218
+ # set_state looks at the old and new states of the promise. It calls handlers and updates tracking
219
+ # data accordingly.
220
+ def set_state(new_state, value_or_reason = nil)
221
+ old_state = @state
222
+
223
+ # First, filter out illegal input
224
+ unless PROMISE_STATES.include?(old_state)
225
+ raise Scarpe::InternalError, "Internal Promise error! Internal state was #{old_state.inspect}! Legal states: #{PROMISE_STATES.inspect}"
226
+ end
227
+
228
+ unless PROMISE_STATES.include?(new_state)
229
+ raise Scarpe::InternalError, "Internal Promise error! Internal state was set to #{new_state.inspect}! " +
230
+ "Legal states: #{PROMISE_STATES.inspect}"
231
+ end
232
+
233
+ if new_state != :fulfilled && new_state != :rejected && !value_or_reason.nil?
234
+ raise Scarpe::InternalError, "Internal promise error! Non-completed state transitions should not specify a value or reason!"
235
+ end
236
+
237
+ # Here's our state-transition grid for what we're doing here.
238
+ # "From" state is on the left, "to" state is on top.
239
+ #
240
+ # U P F R
241
+ #
242
+ # U - 1 . .
243
+ # P X - . .
244
+ # F X X - X
245
+ # R X X X -
246
+ #
247
+ # - Change from same to same, no effect
248
+ # X Illegal for one reason or another, raise error
249
+ # . Great, no problem, run handlers but not @scheduler or @executor
250
+ # 1 Interesting case - if we have an executor, actually change to a *different* state instead
251
+
252
+ # Transitioning from our state to our same state? No-op.
253
+ return if new_state == old_state
254
+
255
+ # Transitioning to any *different* state after being fulfilled or rejected? Nope. Those states are final.
256
+ if complete?
257
+ raise Scarpe::InternalError, "Internal Promise error! Trying to change state from #{old_state.inspect} to #{new_state.inspect}!"
258
+ end
259
+
260
+ if old_state == :pending && new_state == :unscheduled
261
+ raise Shoes::Errors::InvalidAttributeValueError, "Can't change state from :pending to :unscheduled! Scheduling is not reversible!"
262
+ end
263
+
264
+ # The next three checks should all be followed by calling handlers for the newly-changed state.
265
+ # See call_handlers_for below.
266
+
267
+ # Okay, we're getting scheduled.
268
+ if old_state == :unscheduled && new_state == :pending
269
+ @state = new_state
270
+ call_handlers_for(new_state)
271
+
272
+ # It's not impossible for the scheduler to do something that fulfills or rejects the promise.
273
+ # In that case it *also* called the appropriate handlers. Let's get out of here.
274
+ return if @state == :fulfilled || @state == :rejected
275
+
276
+ if @executor
277
+ # In this case we're still pending, but we have a synchronous executor. Let's do it.
278
+ call_executor
279
+ end
280
+
281
+ return
282
+ end
283
+
284
+ # Setting to rejected calls the rejected handlers. But no scheduling ever occurs, so on_scheduled handlers
285
+ # will never be called.
286
+ if new_state == :rejected
287
+ @state = :rejected
288
+ @reason = value_or_reason
289
+ call_handlers_for(new_state)
290
+ end
291
+
292
+ # If we go straight from :unscheduled to :fulfilled we *will* run the on_scheduled callbacks,
293
+ # because we pretend the scheduling *did* occur at some point. Normally that'll be no callbacks,
294
+ # of course.
295
+ #
296
+ # Fun-but-unfortunate trivia: you *can* fulfill a promise before all its parents are fulfilled.
297
+ # If you do, the unfinished parents will result in nil arguments to the on_fulfilled handler,
298
+ # because we have no other value to provide. The scheduler callback will never be called, but
299
+ # the on_scheduled callbacks, if any, will be.
300
+ if new_state == :fulfilled
301
+ @state = :fulfilled
302
+ @returned_value = value_or_reason
303
+ call_handlers_for(new_state)
304
+ end
305
+ end
306
+
307
+ # This private method calls handlers for a new state, removing those handlers
308
+ # since they have now been called. This interacts subtly with set_state()
309
+ # above, particularly in the case of fulfilling a promise without it ever being
310
+ # properly scheduled.
311
+ #
312
+ # The rejected handlers will be cleared if the promise is fulfilled and vice-versa.
313
+ # After rejection, no on_fulfilled handler should ever be called and vice-versa.
314
+ #
315
+ # When we go from :unscheduled to :pending, the scheduler, if any, should be
316
+ # called and cleared. That should *not* happen when going from :unscheduled to
317
+ # :fulfilled.
318
+ def call_handlers_for(state)
319
+ case state
320
+ when :fulfilled
321
+ @on_scheduled.each { |h| h.call(*@parents.map(&:returned_value)) }
322
+ @on_fulfilled.each { |h| h.call(*@parents.map(&:returned_value)) }
323
+ @on_scheduled = @on_rejected = @on_fulfilled = []
324
+ @scheduler = @executor = nil
325
+ when :rejected
326
+ @on_rejected.each { |h| h.call(*@parents.map(&:returned_value)) }
327
+ @on_fulfilled = @on_scheduled = @on_rejected = []
328
+ @scheduler = @executor = nil
329
+ when :pending
330
+ # A scheduler can get an exception. If so, treat it as rejection
331
+ # and the exception as the provided reason.
332
+ if @scheduler
333
+ begin
334
+ @scheduler.call(*@parents.map(&:returned_value))
335
+ rescue => e
336
+ @log.error("Error while running scheduler! #{e.full_message}")
337
+ rejected!(e)
338
+ end
339
+ @scheduler = nil
340
+ end
341
+ @on_scheduled.each { |h| h.call(*@parents.map(&:returned_value)) }
342
+ @on_scheduled = []
343
+ else
344
+ raise Scarpe::InternalError, "Internal error! Trying to call handlers for #{state.inspect}!"
345
+ end
346
+ end
347
+
348
+ def parent_fulfilled!(parent)
349
+ @waiting_on.delete(parent)
350
+
351
+ # Last parent? If so, schedule ourselves.
352
+ if @waiting_on.empty? && !self.complete?
353
+ # This will result in :pending if there's no executor,
354
+ # or fulfilled/rejected if there is an executor.
355
+ set_state(:pending)
356
+ end
357
+ end
358
+
359
+ def parent_rejected!(parent)
360
+ @waiting_on = []
361
+
362
+ unless self.complete?
363
+ # If our parent was rejected and we were waiting on them,
364
+ # now we're rejected too.
365
+ set_state(:rejected)
366
+ end
367
+ end
368
+
369
+ def call_executor
370
+ raise(Scarpe::InternalError, "Internal error! Should not call_executor with no executor!") unless @executor
371
+
372
+ begin
373
+ result = @executor.call(*@parents.map(&:returned_value))
374
+ fulfilled!(result)
375
+ rescue => e
376
+ @log.error("Error running executor! #{e.full_message}")
377
+ rejected!(e)
378
+ end
379
+ ensure
380
+ @executor = nil
381
+ end
382
+
383
+ public
384
+
385
+ # Register a handler to be called when the promise is fulfilled.
386
+ # If called on a fulfilled promise, the handler will be called immediately.
387
+ #
388
+ # @yield Handler to be called on fulfilled
389
+ # @return [Scarpe::Promise] self
390
+ def on_fulfilled(&handler)
391
+ unless handler
392
+ raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_fulfilled!"
393
+ end
394
+
395
+ case @state
396
+ when :fulfilled
397
+ handler.call(*@parents.map(&:returned_value))
398
+ when :pending, :unscheduled
399
+ @on_fulfilled << handler
400
+ when :rejected
401
+ # Do nothing
402
+ end
403
+
404
+ self
405
+ end
406
+
407
+ # Register a handler to be called when the promise is rejected.
408
+ # If called on a rejected promise, the handler will be called immediately.
409
+ #
410
+ # @yield Handler to be called on rejected
411
+ # @return [Scarpe::Promise] self
412
+ def on_rejected(&handler)
413
+ unless handler
414
+ raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_rejected!"
415
+ end
416
+
417
+ case @state
418
+ when :rejected
419
+ handler.call(*@parents.map(&:returned_value))
420
+ when :pending, :unscheduled
421
+ @on_rejected << handler
422
+ when :fulfilled
423
+ # Do nothing
424
+ end
425
+
426
+ self
427
+ end
428
+
429
+ # Register a handler to be called when the promise is scheduled.
430
+ # If called on a promise that was scheduled earlier, the handler
431
+ # will be called immediately.
432
+ #
433
+ # @yield Handler to be called on scheduled
434
+ # @return [Scarpe::Promise] self
435
+ def on_scheduled(&handler)
436
+ unless handler
437
+ raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_scheduled!"
438
+ end
439
+
440
+ # Add a pending handler or call it now
441
+ case @state
442
+ when :fulfilled, :pending
443
+ handler.call(*@parents.map(&:returned_value))
444
+ when :unscheduled
445
+ @on_scheduled << handler
446
+ when :rejected
447
+ # Do nothing
448
+ end
449
+
450
+ self
451
+ end
452
+ end
453
+ Components::Promise = Promise
454
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "scarpe/components/file_helpers"
4
+
5
+ module Scarpe::Components
6
+ class SegmentedFileLoader
7
+ include Scarpe::Components::FileHelpers
8
+
9
+ # Add a new segment type (e.g. "catscradle") with a different
10
+ # file handler.
11
+ #
12
+ # @param type [String] the new name for this segment type
13
+ # @param handler [Object] an object that will be called as obj.call(filename) - often a proc
14
+ # @return <void>
15
+ def add_segment_type(type, handler)
16
+ if segment_type_hash.key?(type)
17
+ raise Shoes::Errors::InvalidAttributeValueError, "Segment type #{type.inspect} already exists!"
18
+ end
19
+
20
+ segment_type_hash[type] = handler
21
+ end
22
+
23
+ # Return an Array of segment type labels, such as "code" and "app_test".
24
+ #
25
+ # @return [Array<String>] the segment type labels
26
+ def segment_types
27
+ segment_type_hash.keys
28
+ end
29
+
30
+ # Normally a Shoes application will want to keep the default segment types,
31
+ # which allow loading a Shoes app and running a test inside. But sometimes
32
+ # the default handler will be wrong and a library will want to register
33
+ # its own "shoes" and "app_test" segment handlers, or not have any at all.
34
+ # For those applications, it makes sense to clear all segment types before
35
+ # registering its own.
36
+ #
37
+ # @return <void>
38
+ def remove_all_segment_types!
39
+ @segment_type_hash = {}
40
+ end
41
+
42
+ # Load a .sca file with an optional YAML frontmatter prefix and
43
+ # multiple file sections which can be treated differently.
44
+ #
45
+ # The file loader acts like a proc, being called with .call()
46
+ # and returning true or false for whether it has handled the
47
+ # file load. This allows chaining loaders in order and the
48
+ # first loader to recognise a file will run it.
49
+ #
50
+ # @param path [String] the file or directory to treat as a Scarpe app
51
+ # @return [Boolean] return true if the file is loaded as a segmented Scarpe app file
52
+ def call(path)
53
+ return false unless path.end_with?(".scas") || path.end_with?(".sspec")
54
+
55
+ file_load(path)
56
+ true
57
+ end
58
+
59
+ # Segment type handlers can call this to perform an operation after the load
60
+ # has completed. This is important for ordering, and because loading a Shoes
61
+ # app often doesn't return. So to have a later section (e.g. tests, additional
62
+ # data) do something that affects Shoes app loading (e.g. set an env var,
63
+ # affect the display service) it's important that app loading take place later
64
+ # in the sequence.
65
+ def after_load(&block)
66
+ @after_load ||= []
67
+ @after_load << block
68
+ end
69
+
70
+ def self.gen_name(segmap)
71
+ ctr = (1..10_000).detect { |i| !segmap.key?("%5d" % i) }
72
+ "%5d" % ctr
73
+ end
74
+
75
+ def self.front_matter_and_segments_from_file(contents)
76
+ require "yaml" # Only load when needed
77
+ require "English"
78
+
79
+ segments = contents.split(/\n-{5,}/)
80
+ front_matter = {}
81
+
82
+ # The very first segment can start with front matter, or with a divider, or with no divider.
83
+ if segments[0].start_with?("---\n") || segments[0] == "---"
84
+ # We have YAML front matter at the start. All later segments will have a divider.
85
+ front_matter = YAML.load segments[0]
86
+ front_matter ||= {} # If the front matter is just the three dashes it returns nil
87
+ segments = segments[1..-1]
88
+ elsif segments[0].start_with?("-----")
89
+ # We have a divider at the start. Great! We're already well set up for this case.
90
+ elsif segments.size == 1
91
+ # No front matter, no divider, a single unnamed segment. No more parsing needed.
92
+ return [{}, { "" => segments[0] }]
93
+ else
94
+ # No front matter, no divider before the first segment, multiple segments.
95
+ # We'll add an artificial divider to the first segment for uniformity.
96
+ segments = ["-----\n" + segments[0]] + segments[1..-1]
97
+ end
98
+
99
+ segmap = {}
100
+ segments.each do |segment|
101
+ if segment =~ /\A-* +(.*?)\n/
102
+ # named segment with separator
103
+ name = ::Regexp.last_match(1)
104
+
105
+ raise("Duplicate segment name: #{name.inspect}!") if segmap.key?(name)
106
+
107
+ segmap[name] = ::Regexp.last_match.post_match
108
+ elsif segment =~ /\A-* *\n/
109
+ # unnamed segment with separator
110
+ segmap[gen_name(segmap)] = ::Regexp.last_match.post_match
111
+ else
112
+ raise Scarpe::InternalError, "Internal error when parsing segments in segmented app file! seg: #{segment.inspect}"
113
+ end
114
+ end
115
+
116
+ [front_matter, segmap]
117
+ end
118
+
119
+ def file_load(path)
120
+ contents = File.read(path)
121
+
122
+ front_matter, segmap = self.class.front_matter_and_segments_from_file(contents)
123
+
124
+ if segmap.empty?
125
+ raise Scarpe::FileContentError, "Illegal segmented Scarpe file: must have at least one code segment, not just front matter!"
126
+ end
127
+
128
+ if front_matter[:segments]
129
+ if front_matter[:segments].size != segmap.size
130
+ raise Scarpe::FileContentError, "Number of front matter :segments must equal number of file segments!"
131
+ end
132
+ else
133
+ if segmap.size > 2
134
+ raise Scarpe::FileContentError, "Segmented files with more than two segments have to specify what they're for!"
135
+ end
136
+
137
+ # Set to default of shoes code only or shoes code and app test code.
138
+ front_matter[:segments] = segmap.size == 2 ? ["shoes", "app_test"] : ["shoes"]
139
+ end
140
+
141
+ # Match up front_matter[:segments] with the segments, or use the default of shoes and app_test.
142
+
143
+ sth = segment_type_hash
144
+ sv = segmap.values
145
+
146
+ tf_specs = []
147
+ front_matter[:segments].each.with_index do |seg_type, idx|
148
+ unless sth.key?(seg_type)
149
+ raise Scarpe::FileContentError, "Unrecognized segment type #{seg_type.inspect}! No matching segment type available!"
150
+ end
151
+
152
+ tf_specs << ["scarpe_#{seg_type}_segment_contents", sv[idx]]
153
+ end
154
+
155
+ with_tempfiles(tf_specs) do |filenames|
156
+ filenames.each.with_index do |filename, idx|
157
+ seg_name = front_matter[:segments][idx]
158
+ sth[seg_name].call(filename)
159
+ end
160
+
161
+ # Need to call @after_load hooks while tempfiles still exist
162
+ if @after_load && !@after_load.empty?
163
+ @after_load.each(&:call)
164
+ @after_load = []
165
+ end
166
+ end
167
+ end
168
+
169
+ # The hash of segment type labels mapped to handlers which will be called.
170
+ # This could be called by a display service, a test framework or similar
171
+ # code that wants to define a non-Scarpe-standard file format.
172
+ #
173
+ # @return [Hash<String, Object>] the name/handler pairs
174
+ def segment_type_hash
175
+ @segment_handlers ||= {
176
+ "shoes" => proc { |seg_file| after_load { load seg_file } },
177
+ "app_test" => proc do |seg_file|
178
+ ENV["SHOES_SPEC_TEST"] = seg_file
179
+ ENV["SHOES_MINITEST_EXPORT_FILE"] = "sspec.json"
180
+ end,
181
+ }
182
+ end
183
+ end
184
+ end
185
+
186
+ # You can add additional segment types to the segmented file loader
187
+ # loader = Scarpe::Components::SegmentedFileLoader.new
188
+ # loader.add_segment_type "capybara", proc { |seg_file| load_file_as_capybara(seg_file) }
189
+ # Shoes.add_file_loader loader
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scarpe; module Components; end; end
4
+ module Scarpe::Components::StringHelpers
5
+ # Cut down from Rails camelize
6
+ def self.camelize(string)
7
+ string = string.sub(/^[a-z\d]*/, &:capitalize)
8
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{::Regexp.last_match(1)}#{::Regexp.last_match(2).capitalize}" }
9
+ end
10
+ end