flor 0.16.1 → 0.16.2

Sign up to get free protection for your applications and to get access to all the features.
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