flor 0.16.1 → 0.16.2

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/CREDITS.md +1 -0
  4. data/Makefile +1 -1
  5. data/README.md +82 -6
  6. data/lib/flor.rb +1 -1
  7. data/lib/flor/conf.rb +19 -6
  8. data/lib/flor/core/executor.rb +45 -16
  9. data/lib/flor/core/node.rb +4 -4
  10. data/lib/flor/core/procedure.rb +40 -0
  11. data/lib/flor/djan.rb +5 -2
  12. data/lib/flor/flor.rb +92 -7
  13. data/lib/flor/id.rb +19 -0
  14. data/lib/flor/migrations/0001_tables.rb +6 -6
  15. data/lib/flor/migrations/0005_pointer_content.rb +20 -0
  16. data/lib/flor/pcore/_apply.rb +103 -57
  17. data/lib/flor/pcore/_att.rb +15 -1
  18. data/lib/flor/pcore/_ref.rb +2 -1
  19. data/lib/flor/pcore/arith.rb +46 -9
  20. data/lib/flor/pcore/break.rb +1 -1
  21. data/lib/flor/pcore/case.rb +41 -0
  22. data/lib/flor/pcore/collect.rb +1 -1
  23. data/lib/flor/pcore/cursor.rb +1 -1
  24. data/lib/flor/pcore/define.rb +32 -6
  25. data/lib/flor/pcore/iterator.rb +12 -0
  26. data/lib/flor/pcore/on_cancel.rb +1 -1
  27. data/lib/flor/pcore/set.rb +14 -4
  28. data/lib/flor/punit/{ccollect.rb → c_collect.rb} +2 -2
  29. data/lib/flor/punit/c_each.rb +11 -0
  30. data/lib/flor/punit/c_for_each.rb +41 -0
  31. data/lib/flor/punit/c_iterator.rb +160 -0
  32. data/lib/flor/punit/c_map.rb +43 -0
  33. data/lib/flor/punit/concurrence.rb +43 -200
  34. data/lib/flor/punit/graft.rb +3 -2
  35. data/lib/flor/punit/m_ram.rb +281 -0
  36. data/lib/flor/unit.rb +1 -0
  37. data/lib/flor/unit/caller.rb +6 -1
  38. data/lib/flor/unit/executor.rb +17 -4
  39. data/lib/flor/unit/ganger.rb +12 -1
  40. data/lib/flor/unit/hloader.rb +251 -0
  41. data/lib/flor/unit/hook.rb +74 -15
  42. data/lib/flor/unit/hooker.rb +9 -12
  43. data/lib/flor/unit/loader.rb +41 -17
  44. data/lib/flor/unit/models.rb +54 -18
  45. data/lib/flor/unit/models/execution.rb +15 -4
  46. data/lib/flor/unit/models/pointer.rb +11 -0
  47. data/lib/flor/unit/scheduler.rb +126 -30
  48. data/lib/flor/unit/spooler.rb +5 -3
  49. data/lib/flor/unit/storage.rb +40 -13
  50. data/lib/flor/unit/waiter.rb +165 -26
  51. data/lib/flor/unit/wlist.rb +98 -5
  52. metadata +10 -4
  53. data/lib/flor/punit/cmap.rb +0 -112
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 325520bb299ef5655cd1e18c0e11a4c7ecf30bd8
4
- data.tar.gz: fb17d66b6254ca47b508007045b172222a336bcb
3
+ metadata.gz: 0c913a70a19f7d01588c3cd927cb529d64538177
4
+ data.tar.gz: 25d6262fe8024c0159a172f392ef65b203f72a1a
5
5
  SHA512:
6
- metadata.gz: 472fe3d93e7853fffcd1fd69de1ef3ce4ea85647d60bcd908f6820270c4d1fab6475084f604b7b36bd611aa75afe0aa676110f088e09db8fd8764e72e44e0737
7
- data.tar.gz: 53770fa93cd46eb71ef3cb76f54fa70f3d042f671595d84a2665bf3712f2817b23adc4bd6d80799469f7a43051f65753dfba4bc6c3d99fa6f5fe5b0da536172b
6
+ metadata.gz: 1d9834e300f1d5158f42027e51966efcfc500edeedf3c7495a42985b77d24dbcd87aafdd1061776c04cc0713181ced907d535cc5c59032b13b355211fab2dad8
7
+ data.tar.gz: ecdd517230e0f0602fb8b617b9d57ce94938864a30ecd7df827dab486de1f85de55ef296f8d0964fc878ccd764685230d2d73bea0e3c9aa53e144ccf52df8dee
@@ -2,6 +2,14 @@
2
2
  # CHANGELOG.md
3
3
 
4
4
 
5
+ ## flor 0.16.2 not yet released
6
+
7
+ - Allow for `[ 'he' 'll' 'o' ] | + join: '.'` (yields "he.ll.o")
8
+ - Allow for `[ 1 2 3 ] | + _` (yields `6`)
9
+ - Make "child_on_error:"/"children_on_error:" a common attribute
10
+ - Ensure "on_cancel" sets only one handler
11
+
12
+
5
13
  ## flor 0.16.1 released 2019-02-05
6
14
 
7
15
  - Depend on Sequel 5 (Sequel 4 and 5 seem OK)
data/CREDITS.md CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  ## Contributors
5
5
 
6
+ * Ryan Scott - https://github.com/Subtletree
6
7
  * Jeffrey Hicks - https://github.com/jrhicks
7
8
  * David Verrier - https://github.com/dverrier
8
9
  * Tsunehisa Doi - https://github.com/dmicky0419
data/Makefile CHANGED
@@ -71,7 +71,7 @@ mk:
71
71
  $(RUBY) -Ilib -e "require 'flor/tools/env'; Flor::Tools::Env.make('tmp', '$(FLOR_ENV)', gitkeep: true)"
72
72
 
73
73
  doc:
74
- $(RUBY) -Imak -r 'doc' -e "make_procedures_doc()"
74
+ $(RUBY) -Imak -r 'doc_procedures' -e "make_doc_procedures()"
75
75
  doct:
76
76
  @$(RUBY) mak/ptree.rb
77
77
 
data/README.md CHANGED
@@ -10,10 +10,11 @@ Flor is a "Ruby workflow engine", if that makes any sense.
10
10
  * [floraison mailing list](https://groups.google.com/forum/#!forum/floraison)
11
11
  * [twitter.com/@flor_workflow](https://twitter.com/flor_workflow)
12
12
 
13
- ## Design
13
+
14
+ ## design
14
15
 
15
16
  * Strives to propose a scheming interpreter for long running executions
16
- * Is written in Ruby a rather straightforward language with at least two
17
+ * Is written in Ruby, a rather straightforward language with at least two
17
18
  wonderful implementations (MRI and JRuby, which is enterprise-friendly)
18
19
  * Stores everything as JSON (if it breaks it's still readable)
19
20
  * Stores in any database supported by [Sequel](http://sequel.jeremyevans.net/)
@@ -22,14 +23,89 @@ Flor is a "Ruby workflow engine", if that makes any sense.
22
23
  * All in all should be easy to maintain (engine itself and executions running
23
24
  on top of it)
24
25
 
25
- ## Quickstart
26
26
 
27
- See [quickstart/](quickstart/).
27
+ ## quickstart
28
+
29
+ This quickstart sets up a flor unit tied to a SQLite database, resets the databse, binds two taskers and then launches a flow execution involving the two taskers. Finally, it prints out the resulting workitem as the execution has just terminated.
30
+
31
+ ```ruby
32
+ require 'flor/unit'
33
+
34
+ #ENV['FLOR_DEBUG'] = 'dbg,sto,stdout' # full sql + flor debug output
35
+ #ENV['FLOR_DEBUG'] = 'dbg,stdout' # flor debug output
36
+ # uncomment to see the flor activity
37
+
38
+ sto_uri = 'sqlite://flor_qs.db'
39
+ sto_uri = 'jdbc:sqlite://flor_qs.db' if RUBY_PLATFORM.match(/java/)
40
+
41
+ flor = Flor::Unit.new(loader: Flor::HashLoader, sto_uri: sto_uri)
42
+ # instantiate flor unit
43
+
44
+ flor.storage.delete_tables
45
+ flor.storage.migrate
46
+ # blank slate database
47
+
48
+ class DemoTasker < Flor::BasicTasker
49
+ def task(message)
50
+ (attd['times'] || 1).times do
51
+ message['payload']['log'] << "#{tasker}: #{task_name}"
52
+ end
53
+ reply
54
+ end
55
+ end
56
+ flor.add_tasker(:alice, DemoTasker)
57
+ flor.add_tasker(:bob, DemoTasker)
58
+ # a simple logging tasker implementation bound under
59
+ # two different tasker names
60
+
61
+ flor.start
62
+ # start the flor unit, so that it can process executions
63
+
64
+ exid = flor.launch(
65
+ %q{
66
+ sequence
67
+ alice 'hello' times: 2
68
+ bob 'world'
69
+ },
70
+ payload: { log: [ "started at #{Time.now}" ] })
71
+ # launch a new execution, one that chains alice and bob work
28
72
 
29
- ## Documentation
73
+ #r = flor.wait(exid, 'terminated')
74
+ r = flor.wait(exid)
75
+ # wait for the execution to terminate or to fail
76
+
77
+ p r['point']
78
+ # "terminated" hopefully
79
+ p r['payload']['log']
80
+ # [ "started at 2019-03-31 10:20:18 +0900",
81
+ # "alice: hello", "alice: hello",
82
+ # "bob: world" ]
83
+ ```
84
+
85
+ This quickstart is at [doc/quickstart0/](doc/quickstart0/), it's a minimal, one-file Ruby quickstart.
86
+
87
+ There is also [doc/quickstart1/](doc/quickstart1/), a more complex example, that shows a flor setup, where taskers and flows are layed out in a flor directory tree.
88
+
89
+
90
+ ## documentation
30
91
 
31
92
  See [doc/](doc/).
32
93
 
94
+ * [doc/procedures/](doc/procedures/#procedures) - the basic building blocks of the flor language
95
+ * [doc/glossary](doc/glossary.md) - words and their meaning in the flor context
96
+ * [doc/patterns](doc/patterns.md) - workflow patterns and their flor (tentative) implementations
97
+
98
+
99
+ ## related projects
100
+
101
+ * [mantor/floristry](https://github.com/mantor/floristry) - visualize and interact with flor through Rails facilities
102
+ * [floraison/pollen](https://github.com/floraison/pollen) - a set of flor hooks that emit over the http
103
+ * [floraison/florist](https://github.com/floraison/florist) - a flor worklist implementation
104
+ * [floraison/flack](https://github.com/floraison/flack) - a flor wrapping [Rack](https://github.com/rack/rack) app
105
+ * [floraison/fugit](https://github.com/floraison/fugit) - a time library for flor and [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler)
106
+ * [floraison/raabro](https://github.com/floraison/raabro) - the PEG library flor uses for its parsing needs
107
+
108
+
33
109
  ## blog posts and presentations
34
110
 
35
111
  * [the flor language](http://jmettraux.skepti.ch/20180927.html?t=the_flor_language) - on the flor workflow definition language itself
@@ -39,7 +115,7 @@ See [doc/](doc/).
39
115
  * [flor 2017](https://speakerdeck.com/jmettraux/flor-2017) - q1 2017 - very dry deck
40
116
 
41
117
 
42
- ## LICENSE
118
+ ## license
43
119
 
44
120
  MIT, see [LICENSE.txt](LICENSE.txt)
45
121
 
@@ -14,7 +14,7 @@ require 'dense'
14
14
 
15
15
  module Flor
16
16
 
17
- VERSION = '0.16.1'
17
+ VERSION = '0.16.2'
18
18
  #VERSION = '1.0.0'
19
19
  end
20
20
 
@@ -48,6 +48,15 @@ module Flor
48
48
  # "reserved" state, messages are put back in the "created" state
49
49
  # (by a running unit (scheduler) if any).
50
50
  #
51
+ # * :db_migration / :sto_migration / :db_migration_dir / :sto_migration_dir
52
+ # (Flor::Storage#migrate option)
53
+ # Points the migrator to its Ruby Sequel migration directory.
54
+ #
55
+ # * :db_sparse_migrations / :sto_sparse_migrations
56
+ # (Flor::Storage#migrate option)
57
+ # Setting this to true is equivalent to calling
58
+ # `unit.storage.migration(allow_missing_migration_files: true)`
59
+ #
51
60
  # And finally:
52
61
  #
53
62
  # * :flor_debug or :debug
@@ -71,8 +80,12 @@ module Flor
71
80
 
72
81
  def prepare(conf, over_conf)
73
82
 
74
- c = conf
75
- c = Flor::ConfExecutor.interpret_path_or_source(c) if c.is_a?(String)
83
+ c =
84
+ case conf
85
+ when String then Flor::ConfExecutor.interpret_path_or_source(conf)
86
+ when Hash then Flor.to_string_keyed_hash(conf)
87
+ else conf
88
+ end
76
89
 
77
90
  fail ArgumentError.new(
78
91
  "cannot extract conf out of #{c.inspect} (#{conf.class})"
@@ -92,10 +105,10 @@ module Flor
92
105
 
93
106
  def get_class(conf, key)
94
107
 
95
- if v = conf[key]
96
- Flor.const_lookup(v)
97
- else
98
- nil
108
+ case v = conf[key]
109
+ when Class then v
110
+ when String then Flor.const_lookup(v)
111
+ else nil
99
112
  end
100
113
  end
101
114
 
@@ -69,18 +69,31 @@ module Flor
69
69
 
70
70
  def trigger_hook(hook, message)
71
71
 
72
- hook.notify(self, message)
72
+ m =
73
+ case
74
+ when hook.respond_to?(:notify) then :notify
75
+ when hook.respond_to?(:on_message) then :on_message
76
+ else :on
77
+ end
78
+ as =
79
+ case hook.method(m).arity
80
+ when 3 then [ @unit, self, message ]
81
+ when 2 then [ self, message ]
82
+ else [ message ]
83
+ end
84
+
85
+ r = hook.send(m, *as)
86
+
87
+ Flor.is_array_of_messages?(r) ? r : []
73
88
  end
74
89
 
75
90
  def trigger_block(block, opts, message)
76
91
 
77
92
  r =
78
- if block.arity == 1
79
- block.call(message)
80
- elsif block.arity == 2
81
- block.call(message, opts)
82
- else
83
- block.call(self, message, opts)
93
+ case block.arity
94
+ when 1 then block.call(message)
95
+ when 2 then block.call(message, opts)
96
+ else block.call(self, message, opts)
84
97
  end
85
98
 
86
99
  r.is_a?(Array) && r.all? { |e| e.is_a?(Hash) } ? r : []
@@ -146,9 +159,10 @@ module Flor
146
159
  # cnid: closure nid
147
160
  # dbg: used to debug messages (useful @node['dbg'] when 'receive')
148
161
 
149
- if oeh = message['on_error_handler']
150
- node['on_error'] = [ [ [ '*' ], oeh ] ]
151
- end
162
+ %w[ error cancel timeout ]
163
+ .each { |k|
164
+ h = message["on_#{k}_handler"]
165
+ node["on_#{k}"] = [ h ] if h }
152
166
 
153
167
  @execution['nodes'][nid] = node
154
168
  end
@@ -228,6 +242,19 @@ module Flor
228
242
  heap = node['heap']
229
243
 
230
244
  heac = Flor::Procedure[heap]
245
+ unless heac
246
+ puts "v" * 80
247
+ puts "===node:"
248
+ p node['nid']
249
+ p heap
250
+ puts "."
251
+ pp node
252
+ puts "===message:"
253
+ p message['point']
254
+ puts "."
255
+ pp message
256
+ puts "." * 80
257
+ end
231
258
  fail NameError.new("unknown procedure #{heap.inspect}") unless heac
232
259
 
233
260
  head = heac.new(self, node, message)
@@ -426,7 +453,7 @@ module Flor
426
453
  ms = []
427
454
  ms += @unit.notify(self, message) # pre
428
455
 
429
- ms += self.send(message['point'], message)
456
+ ms += send(message['point'], message)
430
457
 
431
458
  message['payload'] = message.delete('pld') if message.has_key?('pld')
432
459
  message['consumed'] = Flor.tstamp
@@ -456,11 +483,6 @@ module Flor
456
483
  []
457
484
  end
458
485
 
459
- def entered(message); []; end
460
- def left(message); []; end
461
-
462
- def ceased(message); []; end
463
-
464
486
  def terminated(message)
465
487
 
466
488
  message['vars'] = @execution['nodes']['0']['vars']
@@ -496,6 +518,13 @@ module Flor
496
518
  end
497
519
 
498
520
  def signal(message); []; end
521
+ def entered(message); []; end
522
+ def left(message); []; end
523
+ def ceased(message); []; end
524
+ #
525
+ # Return an empty array of new messages. No direct effect.
526
+ #
527
+ # Some trap, hook, and/or waiter might lie in wait though.
499
528
 
500
529
  def lookup_on_error_parent(message)
501
530
 
@@ -441,12 +441,12 @@ class Flor::Node
441
441
  if node == nil || mod == 'd'
442
442
 
443
443
  return lookup_arg_container(key) \
444
- if mod == '' && %w[ args argv argh ].include?(key)
444
+ if mod == '' && %w[ arga args argv argh argd ].include?(key)
445
445
 
446
- if vwl = node['vwlist']
446
+ if vwl = node['vwlist'] # variable white list
447
447
  return lookup_dvar_container(mod, key) unless var_match?(vwl, key)
448
448
  end
449
- if vbl = node['vblist']
449
+ if vbl = node['vblist'] # variable black list
450
450
  return lookup_dvar_container(mod, key) if var_match?(vbl, key)
451
451
  end
452
452
 
@@ -502,7 +502,7 @@ class Flor::Node
502
502
 
503
503
  val =
504
504
  case key
505
- when 'args', 'argv' then args.collect(&:last)
505
+ when 'arga', 'args', 'argv' then args.collect(&:last)
506
506
  else args.inject({}) { |h, (k, v)| h[k] = v if k; h }
507
507
  end
508
508
 
@@ -615,6 +615,13 @@ class Flor::Procedure < Flor::Node
615
615
  # was considering passing the whole vars back (as 'varz'), but
616
616
  # it got in the way... and it might be heavy
617
617
 
618
+ %w[ error cancel timeout ]
619
+ .each { |k|
620
+ co = @node["child_on_#{k}"]
621
+ next unless co
622
+ kri = [ '*' ]
623
+ m["on_#{k}_handler"] = [ kri, co ] }
624
+
618
625
  [ m ]
619
626
  end
620
627
 
@@ -787,6 +794,10 @@ class Flor::Procedure < Flor::Node
787
794
  #
788
795
  # an idea from "sort" apply, may be useful later on...
789
796
 
797
+ if pl = opts[:payload]
798
+ ms.first['payload'] = pl
799
+ end
800
+
790
801
  ms
791
802
  end
792
803
 
@@ -893,6 +904,35 @@ class Flor::Procedure < Flor::Node
893
904
  wrap_cancel_children('flavour' => 'kill') +
894
905
  wrap_cancelled
895
906
  end
907
+
908
+ def do_add
909
+
910
+ return [] unless node_open?
911
+ # if the node is closed or ended, discard the add message
912
+
913
+ add
914
+ end
915
+
916
+ def add
917
+
918
+ fail Flor::FlorError.new(
919
+ "procedure does not accept add-iteration", self
920
+ ) if message['elements']
921
+
922
+ # TODO fail if the procedure changed
923
+ # could the message contain a SHA for the node as was when the
924
+ # that message got emitted?
925
+ # well, could that be applied to other messages? too late?
926
+
927
+ #puts " === add " + ("=" * 40)
928
+ #pp message
929
+ #puts " === add. " + ("=" * 39)
930
+ t = tree
931
+ i = Flor.child_id(message['tnid'])
932
+ t[1].insert(i, *message['trees'])
933
+
934
+ []
935
+ end
896
936
  end
897
937
 
898
938
 
@@ -88,10 +88,13 @@ module Flor
88
88
 
89
89
  if kt = opts[:keytab]
90
90
  out << ' ' * kt
91
+ :indent
91
92
  elsif opts[:indent]
92
93
  newline(out, opts)
94
+ :newline
93
95
  elsif ! opts[:compact]
94
96
  space(out, opts)
97
+ :space
95
98
  end
96
99
  end
97
100
 
@@ -152,9 +155,9 @@ module Flor
152
155
  c_inf(':', out, opts)
153
156
 
154
157
  kt = key_max_len ? key_max_len - kl : nil
155
- newline_or_space(out, opts.merge(keytab: kt))
158
+ r = newline_or_space(out, opts.merge(keytab: kt))
156
159
 
157
- to_d(v, out, indent(opts, inc: 2, keytab: kt))
160
+ to_d(v, out, indent(opts, inc: 2, keytab: r == :newline ? kt : 1))
158
161
 
159
162
  if ii < x.size - 1
160
163
  c_inf(',', out, opts)
@@ -52,15 +52,29 @@ module Flor
52
52
  end
53
53
  def dupm(h, hh); self.dup_and_merge(h, hh); end
54
54
 
55
- def deep_freeze(o)
55
+ def deep_merge(o0, o1, in_place=false)
56
56
 
57
- if o.is_a?(Array)
58
- o.each { |e| e.freeze }
59
- elsif o.is_a?(Hash)
60
- o.each { |k, v| k.freeze; v.freeze }
57
+ t0 = type(o0)
58
+ t1 = type(o1)
59
+
60
+ return o1 if t1 != t0
61
+
62
+ if t0 == :array
63
+ o1.each_with_index.inject(in_place ? o0 : o0.dup) { |a, (e1, i)|
64
+ a[i] = deep_merge(o0[i], e1, in_place)
65
+ a }
66
+ elsif t0 == :object
67
+ o1.inject(in_place ? o0 : o0.dup) { |h, (k, v1)|
68
+ h[k] = deep_merge(o0[k], v1, in_place)
69
+ h }
70
+ else
71
+ o1
61
72
  end
73
+ end
74
+
75
+ def deep_merge!(o0, o1)
62
76
 
63
- o.freeze
77
+ deep_merge(o0, o1, true)
64
78
  end
65
79
 
66
80
  def false?(o)
@@ -314,7 +328,14 @@ module Flor
314
328
  "not a sub domain #{sub.inspect}"
315
329
  ) unless potential_domain_name?(sub)
316
330
 
317
- sub == dom || sub[0, dom.length + 1] == dom + '.'
331
+ sub_domain?(dom, sub)
332
+ end
333
+
334
+ def sub_domain?(dom, sub)
335
+
336
+ dom == '' ||
337
+ sub == dom ||
338
+ sub[0, dom.length + 1] == dom + '.'
318
339
  end
319
340
 
320
341
  def split_domain_unit(s)
@@ -443,6 +464,36 @@ module Flor
443
464
  o[1].match(/\A\/.*\/[a-zA-Z]*\z/)
444
465
  end
445
466
 
467
+ def is_sqs_tree?(o)
468
+
469
+ o.is_a?(Array) &&
470
+ o[0] == '_sqs' &&
471
+ o[2].is_a?(Integer) &&
472
+ o[1].is_a?(String)
473
+ end
474
+
475
+ def is_num_tree?(o)
476
+
477
+ o.is_a?(Array) &&
478
+ o[0] == '_num' &&
479
+ o[2].is_a?(Integer) &&
480
+ o[1].is_a?(Numeric)
481
+ end
482
+
483
+ def is_ref_tree?(o)
484
+
485
+ o.is_a?(Array) &&
486
+ (o[0] == '_ref' || o[0] == '_reff') &&
487
+ o[2].is_a?(Integer) &&
488
+ o[1].is_a?(Array) &&
489
+ o[1].all? { |e| is_sqs_tree?(e) || is_num_tree?(e) }
490
+ end
491
+
492
+ def ref_to_path(t)
493
+
494
+ t[1].collect { |tt| tt[1].to_s }.join('.')
495
+ end
496
+
446
497
  # Returns [ st, i ], the parent subtree for the final i index of the nid
447
498
  # Used when inserting updated subtrees.
448
499
  #
@@ -543,6 +594,40 @@ module Flor
543
594
 
544
595
  indent == '' ? out.string : nil
545
596
  end
597
+
598
+ def to_string_keyed_hash(o)
599
+
600
+ case o
601
+ when Array
602
+ o.collect { |e| to_string_keyed_hash(e) }
603
+ when Hash
604
+ o.inject({}) { |h, (k, v)| h[k.to_s] = to_string_keyed_hash(v); h }
605
+ else
606
+ o
607
+ end
608
+ end
609
+
610
+ def to_camel_case(s)
611
+
612
+ s.sub(/\A[a-z]/) { |m| m.upcase }.gsub(/_[a-z]/) { |m| m[1, 1].upcase }
613
+ end
614
+
615
+ # Available as `Flor.migration_dir`
616
+ #
617
+ def migration_dir
618
+
619
+ File.absolute_path(
620
+ File.join(
621
+ File.dirname(__FILE__), 'migrations'))
622
+ end
623
+
624
+ def caller_fname
625
+
626
+ caller
627
+ .find { |l| ! l.match(/\/lib\/flor\//) }
628
+ .match(/\A([^:]+:\d+)/)[1]
629
+ .strip
630
+ end
546
631
  end
547
632
  end
548
633