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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +105 -0
- data/Rakefile +6 -0
- data/app/assets/builds/alembic/canvas.js +1 -0
- data/app/assets/builds/alembic/page_builder.css +1 -0
- data/app/assets/builds/alembic/page_builder.js +5 -0
- data/app/assets/stylesheets/alembic/application.css +15 -0
- data/app/controllers/alembic/application_controller.rb +32 -0
- data/app/controllers/alembic/flows_controller.rb +104 -0
- data/app/controllers/alembic/manage/base_controller.rb +16 -0
- data/app/controllers/alembic/manage/canvas_controller.rb +183 -0
- data/app/controllers/alembic/manage/definitions_controller.rb +15 -0
- data/app/controllers/alembic/manage/flows_controller.rb +55 -0
- data/app/controllers/alembic/manage/page_blocks_controller.rb +19 -0
- data/app/controllers/alembic/manage/pages_controller.rb +38 -0
- data/app/controllers/alembic/manage/previews_controller.rb +38 -0
- data/app/controllers/alembic/manage/versions_controller.rb +22 -0
- data/app/controllers/concerns/alembic/manage/authenticates_admin.rb +19 -0
- data/app/controllers/concerns/alembic/manage/draws_canvas.rb +35 -0
- data/app/helpers/alembic/application_helper.rb +11 -0
- data/app/javascript/alembic/canvas/Canvas.jsx +197 -0
- data/app/javascript/alembic/canvas/Connector.jsx +30 -0
- data/app/javascript/alembic/canvas/ConnectorLayer.jsx +31 -0
- data/app/javascript/alembic/canvas/Control.jsx +42 -0
- data/app/javascript/alembic/canvas/Inspector.jsx +44 -0
- data/app/javascript/alembic/canvas/Panel.jsx +67 -0
- data/app/javascript/alembic/canvas/Placeholder.jsx +24 -0
- data/app/javascript/alembic/canvas/Port.jsx +16 -0
- data/app/javascript/alembic/canvas/Records.jsx +34 -0
- data/app/javascript/alembic/canvas/StepCard.jsx +57 -0
- data/app/javascript/alembic/canvas/Toolbar.jsx +19 -0
- data/app/javascript/alembic/canvas/TypePicker.jsx +21 -0
- data/app/javascript/alembic/canvas/changes.js +5 -0
- data/app/javascript/alembic/canvas/choices.js +4 -0
- data/app/javascript/alembic/canvas/flow.js +7 -0
- data/app/javascript/alembic/canvas/flowSender.js +22 -0
- data/app/javascript/alembic/canvas/ids.js +7 -0
- data/app/javascript/alembic/canvas/rows.js +2 -0
- data/app/javascript/alembic/canvas/styles.js +3 -0
- data/app/javascript/alembic/canvas/useConnectors.js +75 -0
- data/app/javascript/alembic/canvas/useFlow.js +17 -0
- data/app/javascript/alembic/canvas.jsx +13 -0
- data/app/javascript/alembic/page_builder/PageBuilder.jsx +18 -0
- data/app/javascript/alembic/page_builder.jsx +10 -0
- data/app/jobs/alembic/application_job.rb +4 -0
- data/app/mailers/alembic/application_mailer.rb +6 -0
- data/app/models/alembic/application_record.rb +5 -0
- data/app/models/alembic/asked.rb +3 -0
- data/app/models/alembic/choice.rb +7 -0
- data/app/models/alembic/flow/admission.rb +19 -0
- data/app/models/alembic/flow/canvas.rb +167 -0
- data/app/models/alembic/flow/change.rb +18 -0
- data/app/models/alembic/flow/condition.rb +24 -0
- data/app/models/alembic/flow/definition.rb +132 -0
- data/app/models/alembic/flow/digest.rb +116 -0
- data/app/models/alembic/flow/document.rb +131 -0
- data/app/models/alembic/flow/drawing.rb +15 -0
- data/app/models/alembic/flow/edge.rb +9 -0
- data/app/models/alembic/flow/edit_history.rb +75 -0
- data/app/models/alembic/flow/layout.rb +88 -0
- data/app/models/alembic/flow/name.rb +15 -0
- data/app/models/alembic/flow/node.rb +5 -0
- data/app/models/alembic/flow/output.rb +23 -0
- data/app/models/alembic/flow/progress/kept.rb +38 -0
- data/app/models/alembic/flow/progress/loose.rb +43 -0
- data/app/models/alembic/flow/progress.rb +11 -0
- data/app/models/alembic/flow/registry.rb +25 -0
- data/app/models/alembic/flow/run.rb +54 -0
- data/app/models/alembic/flow/runner.rb +65 -0
- data/app/models/alembic/flow/settings.rb +80 -0
- data/app/models/alembic/flow/start.rb +13 -0
- data/app/models/alembic/flow/step.rb +42 -0
- data/app/models/alembic/flow/step_type/declaration.rb +118 -0
- data/app/models/alembic/flow/step_type.rb +69 -0
- data/app/models/alembic/flow/summaries.rb +44 -0
- data/app/models/alembic/flow/summary_version.rb +13 -0
- data/app/models/alembic/flow/switch.rb +17 -0
- data/app/models/alembic/flow/terminal.rb +13 -0
- data/app/models/alembic/flow/validator.rb +171 -0
- data/app/models/alembic/flow/version.rb +26 -0
- data/app/models/alembic/flow/violation.rb +9 -0
- data/app/models/alembic/flow.rb +44 -0
- data/app/models/alembic/outputs/band.rb +22 -0
- data/app/models/alembic/outputs/grouped.rb +26 -0
- data/app/models/alembic/outputs/lowest.rb +20 -0
- data/app/models/alembic/outputs/percentage.rb +31 -0
- data/app/models/alembic/outputs/tally.rb +23 -0
- data/app/models/alembic/outputs/weighted_sum.rb +26 -0
- data/app/models/alembic/page.rb +11 -0
- data/app/models/alembic/runner.rb +13 -0
- data/app/models/alembic/steps/question.rb +52 -0
- data/app/models/alembic/summary/output_type.rb +40 -0
- data/app/models/alembic/summary/registry.rb +25 -0
- data/app/models/alembic/summary/report.rb +31 -0
- data/app/models/alembic/summary/result.rb +5 -0
- data/app/models/alembic/summary/run.rb +13 -0
- data/app/models/alembic/summary.rb +15 -0
- data/app/views/alembic/flow/steps/_choosing.html.erb +9 -0
- data/app/views/alembic/flows/complete.html.erb +34 -0
- data/app/views/alembic/flows/show.html.erb +17 -0
- data/app/views/alembic/flows/step.html.erb +27 -0
- data/app/views/alembic/manage/definitions/edit.html.erb +13 -0
- data/app/views/alembic/manage/flows/edit.html.erb +29 -0
- data/app/views/alembic/manage/flows/index.html.erb +32 -0
- data/app/views/alembic/manage/flows/show.html.erb +26 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -0
- data/app/views/alembic/manage/pages/index.html.erb +20 -0
- data/app/views/alembic/manage/pages/show.html.erb +11 -0
- data/app/views/alembic/manage/versions/index.html.erb +41 -0
- data/app/views/layouts/alembic/application.html.erb +17 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20260604215031_create_alembic_diagnostics.rb +14 -0
- data/db/migrate/20260605015051_create_alembic_questions.rb +12 -0
- data/db/migrate/20260605021526_create_alembic_options.rb +14 -0
- data/db/migrate/20260605023308_create_alembic_conditions.rb +11 -0
- data/db/migrate/20260605025623_create_alembic_bands.rb +12 -0
- data/db/migrate/20260605032859_create_alembic_condition_options.rb +10 -0
- data/db/migrate/20260605032900_reshape_conditions_for_placement.rb +11 -0
- data/db/migrate/20260605125119_create_alembic_results.rb +14 -0
- data/db/migrate/20260605130338_create_alembic_rules.rb +10 -0
- data/db/migrate/20260605130339_create_alembic_rule_results.rb +10 -0
- data/db/migrate/20260605210714_add_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260606000855_add_guide_copy_to_alembic_diagnostics.rb +9 -0
- data/db/migrate/20260606005430_create_alembic_nodes.rb +22 -0
- data/db/migrate/20260606010312_create_alembic_build_steps.rb +12 -0
- data/db/migrate/20260606011110_create_alembic_warnings.rb +11 -0
- data/db/migrate/20260804180000_create_alembic_domains.rb +14 -0
- data/db/migrate/20260804180100_add_domain_to_alembic_questions.rb +5 -0
- data/db/migrate/20260822120000_create_alembic_definition_versions.rb +14 -0
- data/db/migrate/20260822120100_backfill_alembic_definition_versions.rb +17 -0
- data/db/migrate/20260822130000_remove_definition_from_alembic_diagnostics.rb +28 -0
- data/db/migrate/20260822140000_create_alembic_responses.rb +14 -0
- data/db/migrate/20260824120000_create_alembic_transitions.rb +10 -0
- data/db/migrate/20260824130000_drop_alembic_builder_tables.rb +17 -0
- data/db/migrate/20260824140000_add_definition_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824160000_add_summary_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824170000_drop_alembic_guide_copy_columns.rb +8 -0
- data/db/migrate/20260825120000_create_alembic_summary_versions.rb +14 -0
- data/db/migrate/20260825130000_add_summary_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825140000_move_summary_definitions_into_versions.rb +26 -0
- data/db/migrate/20260825150000_drop_summary_definition_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825160000_add_summary_version_to_alembic_responses.rb +6 -0
- data/db/migrate/20260825170000_rename_question_settings_in_current_definitions.rb +51 -0
- data/db/migrate/20260825180000_add_document_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825190000_seed_documents_from_current_definitions.rb +29 -0
- data/db/migrate/20260825200000_add_published_version_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825210000_publish_current_definitions.rb +29 -0
- data/db/migrate/20260825220000_add_undone_changes_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825223322_add_status_to_alembic_definition_versions.rb +8 -0
- data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825225015_change_alembic_diagnostic_status_to_availability.rb +16 -0
- data/db/migrate/20260825230000_add_undo_history_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825230500_add_changes_to_alembic_definition_versions.rb +5 -0
- data/db/migrate/20260826230000_rename_alembic_responses_to_flow_runs.rb +5 -0
- data/db/migrate/20260826234500_rename_answers_to_recorded_on_flow_runs.rb +5 -0
- data/db/migrate/20260826235000_rename_definition_versions_to_flow_versions.rb +5 -0
- data/db/migrate/20260826235500_rename_summary_versions_to_flow_summaries.rb +5 -0
- data/db/migrate/20260827000000_rename_diagnostics_to_flows.rb +8 -0
- data/db/migrate/20260831000000_add_persists_to_alembic_flows.rb +5 -0
- data/db/migrate/20260914120000_create_alembic_pages.rb +9 -0
- data/db/migrate/20260915120000_add_blocks_to_alembic_pages.rb +5 -0
- data/lib/alembic/engine.rb +43 -0
- data/lib/alembic/leftover_stylesheet.rb +14 -0
- data/lib/alembic/version.rb +3 -0
- data/lib/alembic.rb +34 -0
- data/lib/tasks/alembic_tasks.rake +4 -0
- 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,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,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,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
|