easy_flow 0.4.9 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 310df5972d6ca11a6862e8c101b79f8802adbe620ee90e3c8764cda70feb9492
4
- data.tar.gz: c43a02f417356eb7bf31e2e53fa220a4d32b7fefed055ed031dda114eccec951
3
+ metadata.gz: af61822fb110201f602335911c189fbaf51fc2c2311a86385a307a75d3584069
4
+ data.tar.gz: 600c9251559a47e18b30122980b62a30c6e6f48e0251b2c480501853d8b1ab49
5
5
  SHA512:
6
- metadata.gz: 82e5b5eede06952c31b553577668d70499b828e576f5acc2e96964a30f8c897c6762d8e450674d5774d2c54034c0309019cf68138ddc45d9de48b165659bfe29
7
- data.tar.gz: 0ad147aae3a60c96b225122d43d7f73e570c275b101b7c74db0bf00389ac01e927026ae5428925c3f9f8887d88a14f1266d82cd5299972b4f53dba7b75e32f9c
6
+ metadata.gz: 52ec7fba925746a9fbd6b6d1c7c04ea5dba2fbd20318fb9e6f8d61f48b8895aca02738304b54ec1df87f10e59011ad22d7718b37faf73dcc73bbc2390ba31e8e
7
+ data.tar.gz: '09bc633fa9648da7631bfc60eaa45a7c678cc0ce2726f35003d7f185683885f48596c0cf4f0796d8fcc84ed3af800fce3ce956f8b3464846757ef653cfcf4866'
@@ -105,13 +105,15 @@ module EasyFlow
105
105
  end
106
106
 
107
107
  def submitted_answers
108
- answers = params.fetch(:answers, {}).permit(*@guide.steps.map(&:id)).to_h.symbolize_keys
109
- asked = @guide.step(params[:asked].to_s)
108
+ given = params.fetch(:answers, {})
109
+ answers = given.permit(*given.keys.select { |key| @guide.step(key) }).to_h.symbolize_keys
110
+ key = params[:asked].to_s.to_sym
111
+ asked = @guide.step(key)
110
112
  return answers unless asked
111
113
 
112
- answer = answers.fetch(asked.id.to_sym, "")
114
+ answer = answers.fetch(key, "")
113
115
  @refused = answer_problem(asked, answer)
114
- @refused ? answers.except(asked.id.to_sym) : answers.merge(asked.id.to_sym => answer)
116
+ @refused ? answers.except(key) : answers.merge(key => answer)
115
117
  end
116
118
 
117
119
  def record_submitted
@@ -139,7 +141,8 @@ module EasyFlow
139
141
  end
140
142
 
141
143
  def asked
142
- runner_for(run.pinned_definition).steps.map(&:id)
144
+ guide = runner_for(run.pinned_definition)
145
+ guide.steps.map(&:id) | [ guide.next_step(run.recorded)&.id ].compact
143
146
  end
144
147
  end
145
148
  end
@@ -10,7 +10,7 @@ module EasyFlow
10
10
  end
11
11
 
12
12
  def step(id)
13
- @document.node(id)
13
+ @document.node(id.to_s.split("@").first)
14
14
  end
15
15
 
16
16
  def steps
@@ -75,22 +75,37 @@ module EasyFlow
75
75
 
76
76
  def walk(state)
77
77
  recorded = []
78
- visited = []
78
+ latest = {}
79
+ visits = Hash.new(0)
80
+ answers_at_visit = {}
81
+ answered = 0
79
82
  cursor = entry
80
83
 
81
- while cursor && !visited.include?(cursor.id)
82
- return [ recorded, cursor ] if pending?(cursor, state)
84
+ while cursor
85
+ return [ recorded, nil ] if visits[cursor.id].positive? && answers_at_visit[cursor.id] == answered
83
86
 
84
- recorded << cursor.id if state.key?(cursor.id)
85
- visited << cursor.id
86
- cursor = successor(cursor, state)
87
+ visits[cursor.id] += 1
88
+ answers_at_visit[cursor.id] = answered
89
+ key = visit_key(cursor.id, visits[cursor.id])
90
+ return [ recorded, cursor.with(id: key) ] if pending?(cursor, key, state)
91
+
92
+ if state.key?(key)
93
+ recorded << key
94
+ latest[cursor.id] = state[key]
95
+ answered += 1 if step_type(cursor)&.awaits_input?
96
+ end
97
+ cursor = successor(cursor, state.merge(latest))
87
98
  end
88
99
 
89
100
  [ recorded, nil ]
90
101
  end
91
102
 
92
- def pending?(node, state)
93
- return false if state.key?(node.id)
103
+ def visit_key(id, visit)
104
+ visit == 1 ? id : "#{id}@#{visit}"
105
+ end
106
+
107
+ def pending?(node, key, state)
108
+ return false if state.key?(key)
94
109
 
95
110
  step_type(node)&.awaits_input? || acts?(node) || step_type(node)&.waits? || false
96
111
  end
@@ -28,7 +28,7 @@ module EasyFlow
28
28
 
29
29
  def run(progress)
30
30
  while (node = @digest.next_step(named(progress.recorded))) && goes_on?(node, named(progress.recorded))
31
- progress.record(node.id, result_of(node, named(progress.recorded)))
31
+ progress.record(node.id, result_of(@digest.step(node.id), named(progress.recorded)))
32
32
  end
33
33
  end
34
34
 
@@ -17,7 +17,7 @@ module EasyFlow
17
17
  end
18
18
 
19
19
  def malformations
20
- missing_edge_targets + missing_edge_sources + duplicate_ids + no_beginning
20
+ missing_edge_targets + missing_edge_sources + duplicate_ids + reserved_ids + no_beginning
21
21
  end
22
22
 
23
23
  private
@@ -41,6 +41,11 @@ module EasyFlow
41
41
  .map { |id, _count| Violation.new(node: id, problem: :duplicate_id) }
42
42
  end
43
43
 
44
+ def reserved_ids
45
+ @document.nodes.map(&:id).select { |id| id.to_s.include?("@") }
46
+ .map { |id| Violation.new(node: id, problem: :reserved_id) }
47
+ end
48
+
44
49
  def no_beginning
45
50
  return [] if known?(@document.entry)
46
51
 
@@ -1,3 +1,3 @@
1
1
  module EasyFlow
2
- VERSION = "0.4.9"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: easy_flow-develop
3
- description: Use PROACTIVELY for adding a step type to easy_flow flows (a step that asks the visitor something, computes a value from earlier answers, picks the next branch, or holds the visitor until something outside the flow has happened), moving a paused run on once what it waits for has happened, refusing a blank or invalid answer to a step with a message, offering an admin a step setting whose options are read from the app's own records, serving a host's flows from the app's own controller and routes, acting when a visitor finishes a flow, and reading a run's recorded answers and question labels — MUST BE USED instead of hand-rolling questionnaire steps, branching logic, number comparisons, hard-coded setting options, answer validation, polling or "come back later" pages, flow controllers or answer lookups.
3
+ description: Use PROACTIVELY for adding a step type to easy_flow flows (a step that asks the visitor something, computes a value from earlier answers, picks the next branch, or holds the visitor until something outside the flow has happened), moving a paused run on once what it waits for has happened, refusing a blank or invalid answer to a step with a message, offering an admin a step setting whose options are read from the app's own records, serving a host's flows from the app's own controller and routes, acting when a visitor finishes a flow, and reading a run's recorded answers and question labels, including every visit's answer when a flow loops back to a question already asked — MUST BE USED instead of hand-rolling questionnaire steps, branching logic, number comparisons, hard-coded setting options, answer validation, polling or "come back later" pages, flow controllers or answer lookups.
4
4
  tools: Read, Write, Edit, Grep
5
5
  scope: guided flows — versioned documents of steps and the connections between them, drawn on a canvas by an admin and run by a visitor one step at a time, with step types the host registers
6
6
  ---
@@ -9,7 +9,7 @@ This local follows the steps below exactly and invents none. Where a step names
9
9
 
10
10
  ## What easy_flow is
11
11
 
12
- A Rails engine for flows an admin draws on a canvas and a visitor runs one step at a time. Each step is an instance of a step type. The engine ships six: start (`:start`), end (`:terminal`), question (`:question`, a question with a list of answers, which the admin can mark required so a blank answer is refused), and three that pick a branch from an earlier answer with no code — condition (`:condition`, is or is not a chosen value), switch (`:switch`, follows the connection labelled with the answer) and compare (`:compare`, reads the answer as a number and checks it by more than, less than, at least or at most against an amount). Before writing a step type to branch on an answer or a number, ask the developer whether an admin placing one of those three on the canvas is enough. Use this local when the app needs a step type of its own, needs a host's flows on its own pages with its own behaviour when a visitor finishes, or needs to read what a visitor answered. It assumes easy_flow is already installed and a host is declared; if not, hand off to `easy_flow-install` first.
12
+ A Rails engine for flows an admin draws on a canvas and a visitor runs one step at a time. Each step is an instance of a step type. The engine ships six: start (`:start`), end (`:terminal`), question (`:question`, a question with a list of answers, which the admin can mark required so a blank answer is refused), and three that pick a branch from an earlier answer with no code — condition (`:condition`, is or is not a chosen value), switch (`:switch`, follows the connection labelled with the answer) and compare (`:compare`, reads the answer as a number and checks it by more than, less than, at least or at most against an amount). An admin may connect a step back to an earlier one, so a flow can ask the same question more than once, and each visit's answer is kept. Before writing a step type to branch on an answer or a number, ask the developer whether an admin placing one of those three on the canvas is enough. Use this local when the app needs a step type of its own, needs a host's flows on its own pages with its own behaviour when a visitor finishes, or needs to read what a visitor answered. It assumes easy_flow is already installed and a host is declared; if not, hand off to `easy_flow-install` first.
13
13
 
14
14
  ## Interface
15
15
 
@@ -102,6 +102,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
102
102
  - `process` returns the value recorded under this step's id. It runs as soon as a visitor reaches the step, before the next step is shown.
103
103
  - A type that declares more than one `output` returns a hash from `process`, keyed by each output's name as a string. A later compare step reads the output the admin picks from that hash.
104
104
  - `route` returns the value that picks the connection to follow; it is compared as a string with the value each leaving connection is labelled with, so `false` follows the connection labelled `false`. Returning `nil` follows the first connection. A route that returns `true` or `false` declares `output :result, type: :boolean, values: [true, false]`.
105
+ - In a flow that loops, the first visit to a step is recorded under its id and each later visit under `<id>@<n>`, where `n` counts from 2. `route` is given the latest visit's answer under the plain step id. `process` and `waits_until` are given the answers as recorded, so the plain id holds the first visit's answer and later visits are under `<id>@2`, `<id>@3` and on. A `process` step reached again records its result under its own `<id>@<n>`. When a computing or waiting type reads an earlier step that can be asked again, ask the developer whether it should read the first visit or the latest.
105
106
  7. For a type that awaits input, write the partial named in `drawn_by`, for example `app/views/flow_steps/_rating.html.erb`. It receives the display object as `step`. It renders only the input, since the page supplies the form, the Next button and the Back button. The input must be named `answers[<step id>]`, or the answer is not recorded:
106
107
 
107
108
  ```erb
@@ -109,6 +110,8 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
109
110
  <%= number_field_tag "answers[#{step.id}]", nil, in: 1..step.scale %>
110
111
  ```
111
112
 
113
+ Name the input from `step.id`, never from a stored or hard-coded id. On a later visit to the step in a loop, the node's id is `<id>@<n>`, and the answer is only recorded against that visit when the input is named with it. A `displays_by` block must pass `node.id` through as the display object's `id` for the same reason.
114
+
112
115
  The input must submit one value. An input that submits an array or a hash, such as checkboxes named `answers[<step id>][]`, is dropped and treated as blank.
113
116
 
114
117
  An answer is recorded as the string the visitor submitted. A compare step still reads a typed answer such as `"12"` as the number it spells, and an answer that is missing or is not a number makes it follow its `false` connection.
@@ -199,10 +202,11 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
199
202
  end
200
203
  ```
201
204
 
202
- - `state_on_path(answers)` — only the answers on the path the visitor actually took, dropping answers left behind by going back.
203
- - `question_text(id)` — the text of a question step. It returns `nil` for a step of any other type.
204
- - `choice_label(id, value)` — the label of the chosen answer, or the value itself when there is no label.
205
- - `steps`, `step(id)`, `next_step(answers)`, `steps_on_path(answers)`, `slug` and `headline` read the rest of the document.
205
+ - `state_on_path(answers)` — only the answers on the path the visitor actually took, in the order they were given, dropping answers left behind by going back. In a flow that loops, it holds every visit: the first under the step id and each later one under `:"<id>@<n>"`, such as `:"job@2"`. Ask the developer whether each visit is shown as its own line or the visits to one question are grouped together; to group them, take the part of the key before `@`.
206
+ - `question_text(id)` — the text of a question step, given either its id or a visit key such as `"job@2"`. It returns `nil` for a step of any other type.
207
+ - `choice_label(id, value)` — the label of the chosen answer, or the value itself when there is no label. It takes a visit key the same way as `question_text`.
208
+ - `step(id)` finds the step for either its id or a visit key.
209
+ - `steps`, `next_step(answers)`, `steps_on_path(answers)`, `slug` and `headline` read the rest of the document.
206
210
 
207
211
  ## Conventions
208
212
 
@@ -215,7 +219,9 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
215
219
  - A type that routes declares the values its output takes, or the canvas offers no connections to label.
216
220
  - Always read a run against `run.pinned_definition`, never the flow's live version, since a run keeps the version it started on after a new one is published.
217
221
  - Always look flows and runs up through a host.
218
- - A step's input field is named `answers[<step id>]` and submits one value. Any other name is ignored, and an array or hash is treated as blank.
222
+ - In a flow that loops, a later visit's answer is keyed `<id>@<n>`, so code that reads answers never assumes one answer per step id. A step id may not contain `@`, and a flow with one is refused when published.
223
+ - A loop that comes back to a step with no answer given since the last visit to it ends the flow there, so every loop needs a step that awaits input.
224
+ - A step's input field is named `answers[<step.id>]` and submits one value. Any other name is ignored, and an array or hash is treated as blank.
219
225
  - An answer the type's `answer_check` refuses is never recorded, and the visitor is shown the same step with the check's message. An answer no check refuses is recorded, a blank one as `""`.
220
226
  - A `waits_until` block is given only the step and the answers recorded so far, so what it waits for must be findable from those.
221
227
  - A type that waits never also declares `awaits_input`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: easy_flow-info
3
- description: Use to learn what easy_flow offers — guided flows an admin draws on a canvas and a visitor runs one step at a time, versioned documents of steps and connections, hosts and the step types each offers its admins, runs, the built-in step types including questions that can require an answer and branching on an answer or comparing a number, step types the host app registers, the checks they make on an answer, and steps that hold a run until something outside the flow has happened, and the settings an admin fills in, including picks whose options come from the app's data.
3
+ description: Use to learn what easy_flow offers — guided flows an admin draws on a canvas and a visitor runs one step at a time, versioned documents of steps and connections, flows that loop back to ask a step again, hosts and the step types each offers its admins, runs, the built-in step types including questions that can require an answer and branching on an answer or comparing a number, step types the host app registers, the checks they make on an answer, and steps that hold a run until something outside the flow has happened, and the settings an admin fills in, including picks whose options come from the app's data.
4
4
  tools: Read
5
5
  scope: guided flows — versioned documents of steps and the connections between them, drawn on a canvas by an admin and run by a visitor one step at a time, with step types the host registers
6
6
  ---
@@ -26,6 +26,7 @@ easy_flow declares no commands of its own for this local. Its surface is split b
26
26
  - To add a step type, give a step type its own rule for refusing an answer, hold a run until something outside the flow has happened, put a flow on the app's own pages, or act on what a visitor answered, use **easy_flow-develop**.
27
27
  - To branch a flow on an answer or on a number the visitor gave, no code is needed: an admin places one of the built-in branching steps on the canvas.
28
28
  - To make a visitor answer a question before going on, no code is needed: an admin marks that question step as required on the canvas.
29
+ - To ask a step again, such as repeating a question until the visitor says they are done, no code is needed: an admin connects a later step back to an earlier one on the canvas.
29
30
  - If the question is only what a word below means, this page is the answer.
30
31
 
31
32
  ## Conventions
@@ -51,8 +52,12 @@ easy_flow declares no commands of its own for this local. Its surface is split b
51
52
  - **Host** — a named part of the app that owns a set of flows. A host sets the visitor layout and the admin layout, how admins are authenticated, how visitors are authorized, how a refusal is answered, and optionally which step types it offers. Flows are always looked up through a host, so one host never sees another's flows.
52
53
  - **Offered step types** — the list of step types a host names for its admins to add. A host that names no list offers every registered step type. The list only narrows the palette.
53
54
  - **Palette** — the step types an admin can add on a host's canvas. It never holds the start step, always holds the end step, and otherwise holds the step types the host offers. A step already in a flow whose type the host does not offer is still drawn and run.
54
- - **Run** — one visitor's pass through a flow. A run is pinned to the version that was live when it started, so publishing a new version does not change a run already under way. It records each answer by step id, and going back discards the last answer on the path.
55
- - **Answers** — the recorded state of a run, keyed by step id. The next step is always worked out from these answers and the connections in the pinned version.
55
+ - **Run** — one visitor's pass through a flow. A run is pinned to the version that was live when it started, so publishing a new version does not change a run already under way. It records each answer by visit, and going back discards the last answer on the path.
56
+ - **Answers** — the recorded state of a run, keyed by visit. The next step is always worked out from these answers and the connections in the pinned version.
57
+ - **Loop** — a connection that leads back to a step the run has already passed, so that step is visited again. Branching steps read the most recent answer to a step, so a loop ends when an answer sends the run down a different connection.
58
+ - **Visit** — one time a run reaches a step. The first visit is keyed by the step id alone, and each later visit is keyed by the step id, an `@`, and the visit number, such as `size@2`. Every visit's answer is kept, and a finished run lists each one.
59
+ - **Stopped loop** — a run that comes back to a step with no new answer given since its last visit there ends at that point rather than cycling forever. Only answers the visitor gives count, so a loop made only of steps that act on their own stops the first time it comes round.
60
+ - **Reserved `@`** — a step id may not contain `@`, since that mark numbers later visits. A flow holding such a step id is reported as invalid.
56
61
  - **Refused answer** — an answer the step's answer check turned down. Nothing is recorded for that step, and the visitor is shown the same step again with the check's message. This works the same whether the flow keeps a stored run or carries its answers in the page.
57
62
  - **Blank answer** — a step the visitor leaves blank. On a step whose answer check does not refuse it, such as a question that is not required, the blank is recorded and the visitor moves on to the next step.
58
63
  - **Checks** — warnings on a flow's shape: an answer value no connection routes, a path nothing follows, and a step that leads nowhere. The engine turns all three on by default. These are separate from answer checks, which look at what a visitor submits.
@@ -51,7 +51,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
51
51
  - `admin_authentication_method` — a method on the base controller, called with no arguments before every admin page. With none set, the admin pages are open to anyone.
52
52
  - `visitor_authorization_method` — a method on the base controller, called with the flow, returning true when the visitor may run it. With none set, every visitor is refused.
53
53
  - `refusal_method` — a method on the base controller, called with the refusal error when a visitor is refused or a flow is unpublished or withdrawn. With none set, the response is `404 Not Found`.
54
- - `offers` — the step types this host's admins can add from the canvas palette, as a list of step type names. With none set, every registered step type is offered. The engine's own names are `question`, `condition`, `switch` and `compare`, A step type the app declares is named by the id passed to `EasyFlow.step`, or after its class when it is a step class, so `Steps::Notify` is `notify`. The End step is offered whether it is listed or not, and the Start step is never offered. Ask the developer which step types each host should offer.
54
+ - `offers` — the step types this host's admins can add from the canvas palette, as a list of step type names. With none set, every registered step type is offered. The engine's own names are `question`, `condition`, `switch` and `compare`. A step type the app declares is named by the id passed to `EasyFlow.step`, or after its class when it is a step class, so `Steps::Notify` is `notify`. The End step is offered whether it is listed or not, and the Start step is never offered. Ask the developer which step types each host should offer.
55
55
 
56
56
  Each method named here must exist on the base controller. Ask the developer to point at it or write it. Do not invent its logic.
57
57
  7. Mount the engine in `config/routes.rb`, once per host. The host name in `defaults` must match a name declared in step 6. When there is more than one mount, give each an `as:` name:
@@ -62,7 +62,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
62
62
  ```
63
63
 
64
64
  A mount whose host name is not declared serves no flows, and its admin pages return `404 Not Found`.
65
- 8. Ask the developer whether visitors' steps should be drawn with the engine's own partial or with one from the app. For the app's own, create a partial, for example `app/views/steps/_step.html.erb`, which receives the step as the local `step`. It is rendered inside the engine's form, so it draws only the fields, and the visitor's answer must be submitted as `answers[<%= step.id %>]`. Then add to the initializer:
65
+ 8. Ask the developer whether visitors' steps should be drawn with the engine's own partial or with one from the app. For the app's own, create a partial, for example `app/views/steps/_step.html.erb`, which receives the step as the local `step`. It is rendered inside the engine's form, so it draws only the fields, and the visitor's answer must be submitted as `answers[<%= step.id %>]`. Always build the field name from `step.id` and never from a fixed id. When a flow loops back and asks a question again, `step.id` names that visit, so each visit's answer is stored apart. Then add to the initializer:
66
66
 
67
67
  ```ruby
68
68
  EasyFlow.draws_with("steps/step")
@@ -32,6 +32,8 @@ sources:
32
32
  - app/models/easy_flow/steps/question.rb
33
33
  - app/models/easy_flow/run.rb
34
34
  - app/models/easy_flow/runner.rb
35
+ - app/models/easy_flow/digest.rb
36
+ - app/models/easy_flow/validator.rb
35
37
  - app/models/easy_flow/question_runner.rb
36
38
  - config/routes.rb
37
39
  - db/migrate/20260924120000_create_easy_flow_definitions.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.9
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider