alembic 0.1.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.
Files changed (168) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +105 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/builds/alembic/canvas.js +1 -0
  6. data/app/assets/builds/alembic/page_builder.css +1 -0
  7. data/app/assets/builds/alembic/page_builder.js +5 -0
  8. data/app/assets/stylesheets/alembic/application.css +15 -0
  9. data/app/controllers/alembic/application_controller.rb +32 -0
  10. data/app/controllers/alembic/flows_controller.rb +104 -0
  11. data/app/controllers/alembic/manage/base_controller.rb +16 -0
  12. data/app/controllers/alembic/manage/canvas_controller.rb +183 -0
  13. data/app/controllers/alembic/manage/definitions_controller.rb +15 -0
  14. data/app/controllers/alembic/manage/flows_controller.rb +55 -0
  15. data/app/controllers/alembic/manage/page_blocks_controller.rb +19 -0
  16. data/app/controllers/alembic/manage/pages_controller.rb +38 -0
  17. data/app/controllers/alembic/manage/previews_controller.rb +38 -0
  18. data/app/controllers/alembic/manage/versions_controller.rb +22 -0
  19. data/app/controllers/concerns/alembic/manage/authenticates_admin.rb +19 -0
  20. data/app/controllers/concerns/alembic/manage/draws_canvas.rb +35 -0
  21. data/app/helpers/alembic/application_helper.rb +11 -0
  22. data/app/javascript/alembic/canvas/Canvas.jsx +197 -0
  23. data/app/javascript/alembic/canvas/Connector.jsx +30 -0
  24. data/app/javascript/alembic/canvas/ConnectorLayer.jsx +31 -0
  25. data/app/javascript/alembic/canvas/Control.jsx +42 -0
  26. data/app/javascript/alembic/canvas/Inspector.jsx +44 -0
  27. data/app/javascript/alembic/canvas/Panel.jsx +67 -0
  28. data/app/javascript/alembic/canvas/Placeholder.jsx +24 -0
  29. data/app/javascript/alembic/canvas/Port.jsx +16 -0
  30. data/app/javascript/alembic/canvas/Records.jsx +34 -0
  31. data/app/javascript/alembic/canvas/StepCard.jsx +57 -0
  32. data/app/javascript/alembic/canvas/Toolbar.jsx +19 -0
  33. data/app/javascript/alembic/canvas/TypePicker.jsx +21 -0
  34. data/app/javascript/alembic/canvas/changes.js +5 -0
  35. data/app/javascript/alembic/canvas/choices.js +4 -0
  36. data/app/javascript/alembic/canvas/flow.js +7 -0
  37. data/app/javascript/alembic/canvas/flowSender.js +22 -0
  38. data/app/javascript/alembic/canvas/ids.js +7 -0
  39. data/app/javascript/alembic/canvas/rows.js +2 -0
  40. data/app/javascript/alembic/canvas/styles.js +3 -0
  41. data/app/javascript/alembic/canvas/useConnectors.js +75 -0
  42. data/app/javascript/alembic/canvas/useFlow.js +17 -0
  43. data/app/javascript/alembic/canvas.jsx +13 -0
  44. data/app/javascript/alembic/page_builder/PageBuilder.jsx +18 -0
  45. data/app/javascript/alembic/page_builder.jsx +10 -0
  46. data/app/jobs/alembic/application_job.rb +4 -0
  47. data/app/mailers/alembic/application_mailer.rb +6 -0
  48. data/app/models/alembic/application_record.rb +5 -0
  49. data/app/models/alembic/asked.rb +3 -0
  50. data/app/models/alembic/choice.rb +7 -0
  51. data/app/models/alembic/flow/admission.rb +19 -0
  52. data/app/models/alembic/flow/canvas.rb +167 -0
  53. data/app/models/alembic/flow/change.rb +18 -0
  54. data/app/models/alembic/flow/condition.rb +24 -0
  55. data/app/models/alembic/flow/definition.rb +132 -0
  56. data/app/models/alembic/flow/digest.rb +116 -0
  57. data/app/models/alembic/flow/document.rb +131 -0
  58. data/app/models/alembic/flow/drawing.rb +15 -0
  59. data/app/models/alembic/flow/edge.rb +9 -0
  60. data/app/models/alembic/flow/edit_history.rb +75 -0
  61. data/app/models/alembic/flow/layout.rb +88 -0
  62. data/app/models/alembic/flow/name.rb +15 -0
  63. data/app/models/alembic/flow/node.rb +5 -0
  64. data/app/models/alembic/flow/output.rb +23 -0
  65. data/app/models/alembic/flow/progress/kept.rb +38 -0
  66. data/app/models/alembic/flow/progress/loose.rb +43 -0
  67. data/app/models/alembic/flow/progress.rb +11 -0
  68. data/app/models/alembic/flow/registry.rb +25 -0
  69. data/app/models/alembic/flow/run.rb +54 -0
  70. data/app/models/alembic/flow/runner.rb +65 -0
  71. data/app/models/alembic/flow/settings.rb +80 -0
  72. data/app/models/alembic/flow/start.rb +13 -0
  73. data/app/models/alembic/flow/step.rb +42 -0
  74. data/app/models/alembic/flow/step_type/declaration.rb +118 -0
  75. data/app/models/alembic/flow/step_type.rb +69 -0
  76. data/app/models/alembic/flow/summaries.rb +44 -0
  77. data/app/models/alembic/flow/summary_version.rb +13 -0
  78. data/app/models/alembic/flow/switch.rb +17 -0
  79. data/app/models/alembic/flow/terminal.rb +13 -0
  80. data/app/models/alembic/flow/validator.rb +171 -0
  81. data/app/models/alembic/flow/version.rb +26 -0
  82. data/app/models/alembic/flow/violation.rb +9 -0
  83. data/app/models/alembic/flow.rb +44 -0
  84. data/app/models/alembic/outputs/band.rb +22 -0
  85. data/app/models/alembic/outputs/grouped.rb +26 -0
  86. data/app/models/alembic/outputs/lowest.rb +20 -0
  87. data/app/models/alembic/outputs/percentage.rb +31 -0
  88. data/app/models/alembic/outputs/tally.rb +23 -0
  89. data/app/models/alembic/outputs/weighted_sum.rb +26 -0
  90. data/app/models/alembic/page.rb +11 -0
  91. data/app/models/alembic/runner.rb +13 -0
  92. data/app/models/alembic/steps/question.rb +52 -0
  93. data/app/models/alembic/summary/output_type.rb +40 -0
  94. data/app/models/alembic/summary/registry.rb +25 -0
  95. data/app/models/alembic/summary/report.rb +31 -0
  96. data/app/models/alembic/summary/result.rb +5 -0
  97. data/app/models/alembic/summary/run.rb +13 -0
  98. data/app/models/alembic/summary.rb +15 -0
  99. data/app/views/alembic/flow/steps/_choosing.html.erb +9 -0
  100. data/app/views/alembic/flows/complete.html.erb +34 -0
  101. data/app/views/alembic/flows/show.html.erb +17 -0
  102. data/app/views/alembic/flows/step.html.erb +27 -0
  103. data/app/views/alembic/manage/definitions/edit.html.erb +13 -0
  104. data/app/views/alembic/manage/flows/edit.html.erb +29 -0
  105. data/app/views/alembic/manage/flows/index.html.erb +32 -0
  106. data/app/views/alembic/manage/flows/show.html.erb +26 -0
  107. data/app/views/alembic/manage/pages/_block_content.html.erb +1 -0
  108. data/app/views/alembic/manage/pages/index.html.erb +20 -0
  109. data/app/views/alembic/manage/pages/show.html.erb +11 -0
  110. data/app/views/alembic/manage/versions/index.html.erb +41 -0
  111. data/app/views/layouts/alembic/application.html.erb +17 -0
  112. data/config/routes.rb +42 -0
  113. data/db/migrate/20260604215031_create_alembic_diagnostics.rb +14 -0
  114. data/db/migrate/20260605015051_create_alembic_questions.rb +12 -0
  115. data/db/migrate/20260605021526_create_alembic_options.rb +14 -0
  116. data/db/migrate/20260605023308_create_alembic_conditions.rb +11 -0
  117. data/db/migrate/20260605025623_create_alembic_bands.rb +12 -0
  118. data/db/migrate/20260605032859_create_alembic_condition_options.rb +10 -0
  119. data/db/migrate/20260605032900_reshape_conditions_for_placement.rb +11 -0
  120. data/db/migrate/20260605125119_create_alembic_results.rb +14 -0
  121. data/db/migrate/20260605130338_create_alembic_rules.rb +10 -0
  122. data/db/migrate/20260605130339_create_alembic_rule_results.rb +10 -0
  123. data/db/migrate/20260605210714_add_definition_to_alembic_diagnostics.rb +5 -0
  124. data/db/migrate/20260606000855_add_guide_copy_to_alembic_diagnostics.rb +9 -0
  125. data/db/migrate/20260606005430_create_alembic_nodes.rb +22 -0
  126. data/db/migrate/20260606010312_create_alembic_build_steps.rb +12 -0
  127. data/db/migrate/20260606011110_create_alembic_warnings.rb +11 -0
  128. data/db/migrate/20260804180000_create_alembic_domains.rb +14 -0
  129. data/db/migrate/20260804180100_add_domain_to_alembic_questions.rb +5 -0
  130. data/db/migrate/20260822120000_create_alembic_definition_versions.rb +14 -0
  131. data/db/migrate/20260822120100_backfill_alembic_definition_versions.rb +17 -0
  132. data/db/migrate/20260822130000_remove_definition_from_alembic_diagnostics.rb +28 -0
  133. data/db/migrate/20260822140000_create_alembic_responses.rb +14 -0
  134. data/db/migrate/20260824120000_create_alembic_transitions.rb +10 -0
  135. data/db/migrate/20260824130000_drop_alembic_builder_tables.rb +17 -0
  136. data/db/migrate/20260824140000_add_definition_cursor_to_alembic_diagnostics.rb +5 -0
  137. data/db/migrate/20260824160000_add_summary_definition_to_alembic_diagnostics.rb +5 -0
  138. data/db/migrate/20260824170000_drop_alembic_guide_copy_columns.rb +8 -0
  139. data/db/migrate/20260825120000_create_alembic_summary_versions.rb +14 -0
  140. data/db/migrate/20260825130000_add_summary_cursor_to_alembic_diagnostics.rb +5 -0
  141. data/db/migrate/20260825140000_move_summary_definitions_into_versions.rb +26 -0
  142. data/db/migrate/20260825150000_drop_summary_definition_from_alembic_diagnostics.rb +5 -0
  143. data/db/migrate/20260825160000_add_summary_version_to_alembic_responses.rb +6 -0
  144. data/db/migrate/20260825170000_rename_question_settings_in_current_definitions.rb +51 -0
  145. data/db/migrate/20260825180000_add_document_to_alembic_diagnostics.rb +6 -0
  146. data/db/migrate/20260825190000_seed_documents_from_current_definitions.rb +29 -0
  147. data/db/migrate/20260825200000_add_published_version_to_alembic_diagnostics.rb +6 -0
  148. data/db/migrate/20260825210000_publish_current_definitions.rb +29 -0
  149. data/db/migrate/20260825220000_add_undone_changes_to_alembic_diagnostics.rb +5 -0
  150. data/db/migrate/20260825223322_add_status_to_alembic_definition_versions.rb +8 -0
  151. data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +5 -0
  152. data/db/migrate/20260825225015_change_alembic_diagnostic_status_to_availability.rb +16 -0
  153. data/db/migrate/20260825230000_add_undo_history_to_alembic_diagnostics.rb +5 -0
  154. data/db/migrate/20260825230500_add_changes_to_alembic_definition_versions.rb +5 -0
  155. data/db/migrate/20260826230000_rename_alembic_responses_to_flow_runs.rb +5 -0
  156. data/db/migrate/20260826234500_rename_answers_to_recorded_on_flow_runs.rb +5 -0
  157. data/db/migrate/20260826235000_rename_definition_versions_to_flow_versions.rb +5 -0
  158. data/db/migrate/20260826235500_rename_summary_versions_to_flow_summaries.rb +5 -0
  159. data/db/migrate/20260827000000_rename_diagnostics_to_flows.rb +8 -0
  160. data/db/migrate/20260831000000_add_persists_to_alembic_flows.rb +5 -0
  161. data/db/migrate/20260914120000_create_alembic_pages.rb +9 -0
  162. data/db/migrate/20260915120000_add_blocks_to_alembic_pages.rb +5 -0
  163. data/lib/alembic/engine.rb +43 -0
  164. data/lib/alembic/leftover_stylesheet.rb +14 -0
  165. data/lib/alembic/version.rb +3 -0
  166. data/lib/alembic.rb +34 -0
  167. data/lib/tasks/alembic_tasks.rake +4 -0
  168. metadata +252 -0
@@ -0,0 +1,88 @@
1
+ module Alembic
2
+ module Flow
3
+ class Layout
4
+ SPREAD = 2
5
+
6
+ def initialize(document)
7
+ @document = document
8
+ end
9
+
10
+ def positions
11
+ rows = depths
12
+ columns = columns_for(rows)
13
+
14
+ rows.to_h { |id, row| [ id, { "row" => row, "column" => columns[id] } ] }
15
+ end
16
+
17
+ private
18
+
19
+ def depths
20
+ walked = walk_from_entry
21
+
22
+ walked.merge(stranded(walked).index_with(next_row(walked)))
23
+ end
24
+
25
+ def walk_from_entry
26
+ seen = {}
27
+ frontier = starting_points.map { |id| [ id, 0 ] }
28
+
29
+ until frontier.empty?
30
+ id, depth = frontier.shift
31
+ next if seen.key?(id)
32
+
33
+ seen[id] = depth
34
+ @document.edges_from(id).each { |edge| frontier << [ edge.to, depth + 1 ] if known?(edge.to) }
35
+ end
36
+
37
+ seen
38
+ end
39
+
40
+ def starting_points
41
+ return [ @document.entry ] if known?(@document.entry)
42
+
43
+ arrived = @document.edges.map(&:to)
44
+ @document.nodes.map(&:id).reject { |id| arrived.include?(id) }
45
+ end
46
+
47
+ def columns_for(rows)
48
+ @columns = {}
49
+ @next_leaf = 0
50
+ rows.keys.each { |id| place(id, branches(rows)) }
51
+ @columns
52
+ end
53
+
54
+ def branches(rows)
55
+ claimed = {}
56
+
57
+ rows.keys.to_h do |id|
58
+ mine = @document.edges_from(id).map(&:to).uniq
59
+ .select { |child| rows[child] == rows[id] + 1 && !claimed.key?(child) }
60
+ mine.each { |child| claimed[child] = true }
61
+ [ id, mine ]
62
+ end
63
+ end
64
+
65
+ def place(id, branches)
66
+ return @columns[id] if @columns.key?(id)
67
+
68
+ mine = branches[id].to_a
69
+ return @columns[id] = (@next_leaf += SPREAD) - SPREAD if mine.empty?
70
+
71
+ spans = mine.map { |child| place(child, branches) }
72
+ @columns[id] = (spans.min + spans.max) / 2
73
+ end
74
+
75
+ def stranded(walked)
76
+ @document.nodes.map(&:id).uniq - walked.keys
77
+ end
78
+
79
+ def next_row(walked)
80
+ (walked.values.max || -1) + 1
81
+ end
82
+
83
+ def known?(id)
84
+ @document.node(id).present?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,15 @@
1
+ module Alembic
2
+ module Flow
3
+ module Name
4
+ def self.of(node, registry = Flow.registry)
5
+ return "" unless node
6
+
7
+ named(node, registry) || node.id
8
+ end
9
+
10
+ def self.named(node, registry)
11
+ registry.fetch(node.type).name_of(node) if registry.registered?(node.type)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Alembic
2
+ module Flow
3
+ Node = Data.define(:id, :type, :config)
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ module Alembic
2
+ module Flow
3
+ Output = Data.define(:name, :label, :type, :values, :from) do
4
+ def initialize(name:, label: nil, type: :string, values: nil, from: nil)
5
+ raise UnknownOutputType, "#{type} is not one of #{OUTPUT_TYPES.join(', ')}" unless OUTPUT_TYPES.include?(type)
6
+
7
+ super(name: name, label: label.presence || name.to_s.humanize, type: type, values: values, from: from)
8
+ end
9
+
10
+ def values_for(node)
11
+ Array(values.respond_to?(:call) ? values.call(node) : values).map { |value| taken(value) }
12
+ end
13
+
14
+ private
15
+
16
+ def taken(value)
17
+ return value if value.is_a?(Hash)
18
+
19
+ { "value" => value, "label" => value.to_s }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module Alembic
2
+ module Flow
3
+ module Progress
4
+ class Kept
5
+ def initialize(run)
6
+ @run = run
7
+ end
8
+
9
+ def definition
10
+ @run.pinned_definition
11
+ end
12
+
13
+ def summary_of(state)
14
+ return [] if @run.pinned_summary.blank?
15
+
16
+ Summary::Report.new(@run.pinned_summary)
17
+ .results(Summary::Run.new(state: state, steps: @run.pinned_steps))
18
+ end
19
+
20
+ def recorded
21
+ @run.recorded
22
+ end
23
+
24
+ def record(id, value)
25
+ @run.record(id.to_sym, value)
26
+ end
27
+
28
+ def finish(_state)
29
+ @run
30
+ end
31
+
32
+ def discard_last
33
+ @run.discard_last
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ module Alembic
2
+ module Flow
3
+ module Progress
4
+ class Loose
5
+ def initialize(flow, answers, definition = nil)
6
+ @flow = flow
7
+ @answers = answers.to_h.symbolize_keys
8
+ @definition = definition
9
+ end
10
+
11
+ def definition
12
+ @definition || @flow.live_definition
13
+ end
14
+
15
+ def summary_of(state)
16
+ return [] unless @flow.summaries.any?
17
+
18
+ @flow.summaries.of(state)
19
+ end
20
+
21
+ def recorded
22
+ @answers
23
+ end
24
+
25
+ def record(id, value)
26
+ @answers = @answers.merge(id.to_sym => value)
27
+ end
28
+
29
+ def finish(state)
30
+ return unless @flow.on_finish?
31
+
32
+ Run.start(@flow).tap { |run| run.update!(recorded: state) }
33
+ end
34
+
35
+ def discard_last
36
+ last = Runner.new(@flow.live_definition).state_on_path(@answers).keys.last
37
+
38
+ @answers = @answers.except(last) if last
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ module Alembic
2
+ module Flow
3
+ module Progress
4
+ def self.for(flow, run: nil, answers: {}, definition: nil)
5
+ return Kept.new(run) if run
6
+
7
+ Loose.new(flow, answers, definition)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module Alembic
2
+ module Flow
3
+ class Registry
4
+ def initialize
5
+ @step_types = {}
6
+ end
7
+
8
+ def register(step_type)
9
+ @step_types[step_type.id.to_sym] = step_type
10
+ end
11
+
12
+ def fetch(id)
13
+ @step_types.fetch(id.to_sym) { raise UnknownStepType, "no step type registered as #{id}" }
14
+ end
15
+
16
+ def registered?(id)
17
+ id.present? && @step_types.key?(id.to_sym)
18
+ end
19
+
20
+ def step_types
21
+ @step_types.values
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ module Alembic
2
+ module Flow
3
+ class Run < ApplicationRecord
4
+ self.table_name = "alembic_flow_runs"
5
+
6
+ belongs_to :flow, class_name: "Alembic::Flow::Definition"
7
+ belongs_to :definition_version, class_name: "Alembic::Flow::Version"
8
+ belongs_to :summary_version, class_name: "Alembic::Flow::SummaryVersion", optional: true
9
+ belongs_to :owner, polymorphic: true, optional: true
10
+
11
+ def self.start(flow)
12
+ create!(flow: flow, definition_version: flow.live_version,
13
+ summary_version: flow.summaries.current_version)
14
+ end
15
+
16
+ def record(step_id, value)
17
+ update!(recorded: recorded.merge(step_id => value))
18
+ end
19
+
20
+ def recorded
21
+ super.to_h.symbolize_keys
22
+ end
23
+
24
+ def pinned_definition
25
+ definition_version.definition.to_h
26
+ end
27
+
28
+ def pinned_summary
29
+ summary_version&.summary.to_h
30
+ end
31
+
32
+ def next_step(state)
33
+ digest.next_step(state.transform_keys(&:to_s))
34
+ end
35
+
36
+ def walked(state)
37
+ digest.state_on_path(state.transform_keys(&:to_s))
38
+ end
39
+
40
+ def digest
41
+ @digest ||= Digest.new(Document.new(pinned_definition))
42
+ end
43
+
44
+ def discard_last
45
+ last = walked(recorded).keys.map(&:to_sym).last
46
+ update!(recorded: recorded.except(last)) if last
47
+ end
48
+
49
+ def pinned_steps
50
+ Array(definition_version.definition.to_h["nodes"]).index_by { |node| node["id"] }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ module Alembic
2
+ module Flow
3
+ class Runner
4
+ def initialize(document, registry: Flow.registry)
5
+ @document = document.to_h
6
+ @registry = registry
7
+ @digest = Digest.new(Document.new(@document, registry: registry), registry: registry)
8
+ end
9
+
10
+ def slug
11
+ @document["slug"]
12
+ end
13
+
14
+ def headline
15
+ @document["headline"]
16
+ end
17
+
18
+ def steps
19
+ @digest.steps
20
+ end
21
+
22
+ def step(id)
23
+ @digest.step(id.to_s)
24
+ end
25
+
26
+ def next_step(state)
27
+ shown(@digest.next_step(named(state)))
28
+ end
29
+
30
+ def run(progress)
31
+ while (node = @digest.next_step(named(progress.recorded))) && acts?(node)
32
+ progress.record(node.id, @registry.fetch(node.type).process(node, named(progress.recorded)))
33
+ end
34
+ end
35
+
36
+ def drawing_at(state)
37
+ Drawing.of(@digest.next_step(named(state)), @registry)
38
+ end
39
+
40
+ def state_on_path(state)
41
+ @digest.state_on_path(named(state)).symbolize_keys
42
+ end
43
+
44
+ def steps_on_path(state)
45
+ state_on_path(state).keys.map { |id| shown(@digest.step(id.to_s)) }
46
+ end
47
+
48
+ private
49
+
50
+ def acts?(node)
51
+ @registry.registered?(node.type) && @registry.fetch(node.type).acts?
52
+ end
53
+
54
+ def shown(node)
55
+ return node unless node && @registry.registered?(node.type)
56
+
57
+ @registry.fetch(node.type).display_of(node) || node
58
+ end
59
+
60
+ def named(state)
61
+ state.transform_keys(&:to_s)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ module Alembic
2
+ module Flow
3
+ class Settings
4
+ attr_reader :fields, :labels, :record_fields, :record_labels, :choices, :drawn_from, :outputs_of
5
+
6
+ def initialize(fields: {}, labels: {}, record_fields: {}, record_labels: {}, choices: {},
7
+ limits: {}, checks: {}, required: [], drawn_from: {}, outputs_of: {})
8
+ @fields = fields
9
+ @labels = labels
10
+ @record_fields = record_fields
11
+ @record_labels = record_labels
12
+ @choices = choices
13
+ @limits = limits
14
+ @checks = checks
15
+ @required = required
16
+ @drawn_from = drawn_from
17
+ @outputs_of = outputs_of
18
+ end
19
+
20
+ def naming_steps
21
+ fields.select { |_name, type| type == :previous_step }.keys
22
+ end
23
+
24
+ def required
25
+ fields.keys.select { |name| @required.include?(name) || naming_steps.include?(name) }
26
+ end
27
+
28
+ def requirements_for(config)
29
+ naming_steps.map { |name| config.to_h[name.to_s] }.compact_blank
30
+ end
31
+
32
+ def coerce(config)
33
+ config.to_h.to_h { |name, value| [ name, cast(fields[name.to_sym], value, record_fields[name.to_sym]) ] }
34
+ end
35
+
36
+ def objections(config)
37
+ config.to_h.flat_map { |name, value| objections_to(name.to_sym, value) }.compact
38
+ end
39
+
40
+ private
41
+
42
+ def objections_to(name, value)
43
+ [ unoffered(name, value), over_limit(name, value), refused(name, value) ]
44
+ end
45
+
46
+ def unoffered(name, value)
47
+ offered = choices[name]
48
+ return unless offered
49
+
50
+ stray = Array(value).find { |chosen| offered.exclude?(chosen) }
51
+ "#{labels[name]} does not offer #{stray}" if stray
52
+ end
53
+
54
+ def over_limit(name, value)
55
+ allowed = @limits[name]
56
+ return unless allowed && Array(value).size > allowed
57
+
58
+ "#{labels[name]} takes at most #{allowed}"
59
+ end
60
+
61
+ def refused(name, value)
62
+ @checks[name]&.call(value)
63
+ end
64
+
65
+ def cast(type, value, holds = nil)
66
+ case type
67
+ when :integer then Integer(value, exception: false)
68
+ when :float then Float(value, exception: false)
69
+ when :boolean then ActiveModel::Type::Boolean.new.cast(value).present?
70
+ when :list then Array(value).map { |entry| cast_entry(entry, holds.to_h) }
71
+ else value
72
+ end
73
+ end
74
+
75
+ def cast_entry(entry, holds)
76
+ entry.to_h.to_h { |name, value| [ name, cast(holds[name.to_sym], value) ] }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ module Alembic
2
+ module Flow
3
+ class Start
4
+ include Step
5
+
6
+ step_name "Start"
7
+
8
+ begins_here
9
+
10
+ names_by { |_node| "Start" }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ module Alembic
2
+ module Flow
3
+ module Step
4
+ extend ActiveSupport::Concern
5
+
6
+ WORDS = (StepType::Declaration.public_instance_methods(false) - [ :to_step_type ]).freeze
7
+ BEHAVIOURS = %i[route process].freeze
8
+
9
+ included do
10
+ class_attribute :declarations, default: []
11
+ end
12
+
13
+ class_methods do
14
+ WORDS.each do |word|
15
+ define_method(word) do |*arguments, **options, &block|
16
+ self.declarations += [ [ word, arguments, options, block ] ]
17
+ end
18
+ end
19
+
20
+ def step_type
21
+ spoken = declarations + behaviours
22
+ StepType.define(step_type_id) do
23
+ spoken.each { |word, arguments, options, block| public_send(word, *arguments, **options, &block) }
24
+ end
25
+ end
26
+
27
+ def behaviours
28
+ BEHAVIOURS.select { |word| method_defined?(word) }
29
+ .map { |word| [ word, [], {}, ->(node, state) { new.public_send(word, node, state) } ] }
30
+ end
31
+
32
+ def register(registry = Flow.registry)
33
+ registry.register(step_type)
34
+ end
35
+
36
+ def step_type_id
37
+ name.demodulize.underscore.to_sym
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,118 @@
1
+ module Alembic
2
+ module Flow
3
+ class StepType
4
+ class Declaration
5
+ def initialize(id)
6
+ @id = id
7
+ @step_name = id.to_s
8
+ @fields = {}
9
+ @labels = {}
10
+ @record_fields = {}
11
+ @record_labels = {}
12
+ @choices = {}
13
+ @limits = {}
14
+ @checks = {}
15
+ @drawn_from = {}
16
+ @outputs_of = {}
17
+ @declared_outputs = []
18
+ @required = []
19
+ @awaits_input = false
20
+ @ends_here = false
21
+ @begins_here = false
22
+ end
23
+
24
+ def output(name, type: :string, label: nil, values: nil, from: nil)
25
+ @declared_outputs += [ Output.new(name: name, type: type, label: label, values: values, from: from) ]
26
+ end
27
+
28
+ def displays_by(&display)
29
+ @display = display
30
+ end
31
+
32
+ def drawn_by(template)
33
+ @drawn_by = template
34
+ end
35
+
36
+ def process(&behaviour)
37
+ @behaviour = behaviour
38
+ end
39
+
40
+ def route(&routing)
41
+ @routing = routing
42
+ end
43
+
44
+ def step_name(value)
45
+ @step_name = value
46
+ end
47
+
48
+ def awaits_input
49
+ @awaits_input = true
50
+ end
51
+
52
+ def ends_here
53
+ @ends_here = true
54
+ end
55
+
56
+ def begins_here
57
+ @begins_here = true
58
+ end
59
+
60
+ def names_by(field = nil, &naming)
61
+ @naming_field = field
62
+ @naming = naming
63
+ end
64
+
65
+ def setting(name, type: nil, from: nil, outputs_of: nil, label: nil, options: nil, limit: nil, check: nil, required: false, &entries)
66
+ type ||= :from_step if from
67
+ type ||= :from_step if outputs_of
68
+ raise UnknownFieldType, "#{type} is not one of #{FIELD_TYPES.join(', ')}" unless FIELD_TYPES.include?(type)
69
+
70
+ @drawn_from[name] = from if from
71
+ @outputs_of[name] = outputs_of if outputs_of
72
+ @required += [ name ] if required
73
+
74
+ declare_entries(name, entries) if type == :list
75
+ declare_choices(name, type, options)
76
+ @limits[name] = limit if limit
77
+ @checks[name] = check if check
78
+ @labels[name] = label.presence || name.to_s.humanize
79
+ @fields[name] = type
80
+ end
81
+
82
+ protected
83
+
84
+ attr_reader :fields, :labels
85
+
86
+ private
87
+
88
+ def declare_choices(name, type, options)
89
+ return @choices[name] = options if options.present?
90
+ raise UnknownFieldType, "a #{type} must offer options" if %i[select multi_select].include?(type)
91
+ end
92
+
93
+ def declare_entries(name, entries)
94
+ raise UnknownFieldType, "a list must say what an entry holds" if entries.nil?
95
+
96
+ declared = Declaration.new(:entry).tap { |entry| entry.instance_eval(&entries) }
97
+ @record_fields[name] = declared.fields
98
+ @record_labels[name] = declared.labels
99
+ end
100
+
101
+ public
102
+
103
+ def to_step_type
104
+ StepType.new(id: @id, step_name: @step_name, settings: settings, awaits_input: @awaits_input,
105
+ ends_here: @ends_here, begins_here: @begins_here, behaviour: @behaviour, routing: @routing,
106
+ display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
107
+ outputs: @declared_outputs)
108
+ end
109
+
110
+ def settings
111
+ Settings.new(fields: @fields, labels: @labels, record_fields: @record_fields,
112
+ record_labels: @record_labels, choices: @choices, limits: @limits, checks: @checks,
113
+ required: @required, drawn_from: @drawn_from, outputs_of: @outputs_of)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,69 @@
1
+ module Alembic
2
+ module Flow
3
+ class StepType
4
+ def self.define(id, &declaration)
5
+ Declaration.new(id).tap { |decl| decl.instance_eval(&declaration) }.to_step_type
6
+ end
7
+
8
+ attr_reader :id, :step_name, :settings, :naming_field, :naming, :outputs, :drawn_by
9
+
10
+ def initialize(id:, step_name:, settings:, awaits_input:, behaviour:, routing:,
11
+ ends_here: false, begins_here: false, display: nil, drawn_by: nil,
12
+ naming_field: nil, naming: nil, outputs: [])
13
+ @id = id
14
+ @step_name = step_name
15
+ @settings = settings
16
+ @awaits_input = awaits_input
17
+ @ends_here = ends_here
18
+ @begins_here = begins_here
19
+ @behaviour = behaviour
20
+ @routing = routing
21
+ @display = display
22
+ @drawn_by = drawn_by
23
+ @naming_field = naming_field
24
+ @naming = naming
25
+ @outputs = outputs
26
+ end
27
+
28
+ def display_of(node)
29
+ @display&.call(node)
30
+ end
31
+
32
+ def process(node, state)
33
+ @behaviour&.call(node, state)
34
+ end
35
+
36
+ def route(node, state)
37
+ @routing&.call(node, state)
38
+ end
39
+
40
+ def name_of(node)
41
+ @naming&.call(node).presence || node.config[naming_field.to_s].presence
42
+ end
43
+
44
+ def acts?
45
+ @behaviour.present?
46
+ end
47
+
48
+ def routes?
49
+ @routing.present?
50
+ end
51
+
52
+ def values_of(name, node)
53
+ outputs.find { |output| output.name.to_s == name.to_s }&.values_for(node).to_a
54
+ end
55
+
56
+ def awaits_input?
57
+ @awaits_input
58
+ end
59
+
60
+ def ends_here?
61
+ @ends_here
62
+ end
63
+
64
+ def begins_here?
65
+ @begins_here
66
+ end
67
+ end
68
+ end
69
+ end