flowengine-rails 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/.claude/branch-name.sh +49 -0
- data/.claude/commands/create-pr.md +93 -0
- data/.claude/commands/stash-unstaged.md +21 -0
- data/.claude/commands/unstash-unstaged.md +15 -0
- data/.claude/settings.json +72 -0
- data/.rubocop_todo.yml +17 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +153 -0
- data/LICENSE.txt +21 -0
- data/README.md +294 -0
- data/Rakefile +43 -0
- data/app/assets/javascripts/flow_engine/embed.js +44 -0
- data/app/assets/javascripts/flow_engine/progress_controller.js +17 -0
- data/app/assets/javascripts/flow_engine/step_controller.js +22 -0
- data/app/assets/stylesheets/flow_engine/application.css +569 -0
- data/app/controllers/flow_engine/admin/definitions_controller.rb +95 -0
- data/app/controllers/flow_engine/application_controller.rb +41 -0
- data/app/controllers/flow_engine/sessions_controller.rb +91 -0
- data/app/helpers/flow_engine/sessions_helper.rb +20 -0
- data/app/models/flow_engine/application_record.rb +8 -0
- data/app/models/flow_engine/flow_definition.rb +75 -0
- data/app/models/flow_engine/flow_session.rb +98 -0
- data/app/views/flow_engine/admin/definitions/_form.html.erb +27 -0
- data/app/views/flow_engine/admin/definitions/edit.html.erb +5 -0
- data/app/views/flow_engine/admin/definitions/index.html.erb +41 -0
- data/app/views/flow_engine/admin/definitions/mermaid.html.erb +9 -0
- data/app/views/flow_engine/admin/definitions/new.html.erb +5 -0
- data/app/views/flow_engine/admin/definitions/show.html.erb +25 -0
- data/app/views/flow_engine/sessions/completed.html.erb +34 -0
- data/app/views/flow_engine/sessions/new.html.erb +17 -0
- data/app/views/flow_engine/sessions/show.html.erb +26 -0
- data/app/views/flow_engine/sessions/steps/_boolean.html.erb +10 -0
- data/app/views/flow_engine/sessions/steps/_display.html.erb +4 -0
- data/app/views/flow_engine/sessions/steps/_multi_select.html.erb +8 -0
- data/app/views/flow_engine/sessions/steps/_number.html.erb +3 -0
- data/app/views/flow_engine/sessions/steps/_number_matrix.html.erb +13 -0
- data/app/views/flow_engine/sessions/steps/_single_select.html.erb +8 -0
- data/app/views/flow_engine/sessions/steps/_text.html.erb +11 -0
- data/app/views/flow_engine/sessions/steps/_unknown.html.erb +4 -0
- data/app/views/layouts/flow_engine/application.html.erb +26 -0
- data/app/views/layouts/flow_engine/embed.html.erb +30 -0
- data/config/routes.rb +22 -0
- data/db/migrate/01_create_flow_engine_definitions.rb +18 -0
- data/db/migrate/02_create_flow_engine_sessions.rb +18 -0
- data/exe/flowengine-rails +4 -0
- data/justfile +49 -0
- data/lefthook.yml +16 -0
- data/lib/flowengine/rails/configuration.rb +23 -0
- data/lib/flowengine/rails/dsl_loader.rb +35 -0
- data/lib/flowengine/rails/engine.rb +26 -0
- data/lib/flowengine/rails/version.rb +7 -0
- data/lib/flowengine/rails.rb +27 -0
- data/lib/generators/flow_engine/flow/flow_generator.rb +29 -0
- data/lib/generators/flow_engine/flow/templates/flow_definition.rb.tt +27 -0
- data/lib/generators/flow_engine/flow/templates/seed_task.rake.tt +22 -0
- data/lib/generators/flow_engine/install/install_generator.rb +34 -0
- data/lib/generators/flow_engine/install/templates/initializer.rb +25 -0
- data/log/.gitkeep +0 -0
- data/sig/flowengine/rails.rbs +6 -0
- metadata +164 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8ff826ab26eb55ed836e66300baf943197a13f17b4d44886ab784a303f919978
|
|
4
|
+
data.tar.gz: dbd3b01d8eff30c0ed0b7b9054ccc5769161afb350f14cdce34bd6b62a6dbf70
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91d37ce06fcaee18b93c05e5a79bafdd891c9906238bc5ad542f1866c3d9abee4263706d70b65c948b76b1fdb999bb74be67a4fdfc5bcdf2071146163960536d
|
|
7
|
+
data.tar.gz: 6b6f9f96accc3f9be1dd01d7b35d8fb6e7a634016d5eb5dccf0639ebc0c2dd39d2136c21dcb719e11433faeae04decedbc0261a9e51dc96b76ed25515b935229
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ANSI color codes
|
|
4
|
+
BOLD_YELLOW='\033[1;33m'
|
|
5
|
+
BOLD_RED='\033[1;31m'
|
|
6
|
+
RESET='\033[0m'
|
|
7
|
+
|
|
8
|
+
while true; do
|
|
9
|
+
if [[ -z "$*" ]]; then
|
|
10
|
+
# Prompt user (to STDERR)
|
|
11
|
+
echo -n "Please name the branch for your changes: " >&2
|
|
12
|
+
|
|
13
|
+
# Print bold yellow escape sequence
|
|
14
|
+
echo -ne "${BOLD_YELLOW}" >&2
|
|
15
|
+
|
|
16
|
+
# Read user input
|
|
17
|
+
read branch_input
|
|
18
|
+
else
|
|
19
|
+
branch_input="$*"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Reset color
|
|
23
|
+
echo -ne "${RESET}" >&2
|
|
24
|
+
|
|
25
|
+
# Transform the input:
|
|
26
|
+
# 1. Replace non-alphanumeric (except / and space) with dash
|
|
27
|
+
# 2. Replace sequences of multiple dashes with single dash
|
|
28
|
+
# 3. Convert to lowercase
|
|
29
|
+
branch_name="$(echo "${branch_input}" |
|
|
30
|
+
sed -E 's/[^a-zA-Z0-9/-]/-/g' |
|
|
31
|
+
sed -E 's/-+/-/g; ' |
|
|
32
|
+
tr '[:upper:]' '[:lower:]')"
|
|
33
|
+
|
|
34
|
+
# Prepend $USER/ if not already present
|
|
35
|
+
if [[ ! "$branch_name" =~ ^${USER}/ ]]; then
|
|
36
|
+
branch_name="${USER}/${branch_name}"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check length
|
|
40
|
+
if [ ${#branch_name} -gt 50 ]; then
|
|
41
|
+
echo -e "${BOLD_RED}Error: Branch name is too long (${#branch_name} characters, max 50)${RESET}" >&2
|
|
42
|
+
echo >&2
|
|
43
|
+
continue
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Output the branch name to STDOUT
|
|
47
|
+
echo "${branch_name}"
|
|
48
|
+
break
|
|
49
|
+
done
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
description: "Create a pull request from current branch or staged changes"
|
|
4
|
+
allowed-tools:
|
|
5
|
+
|
|
6
|
+
- Bash(./.claude/branch-name.sh)
|
|
7
|
+
- Bash(just check-all:\*)
|
|
8
|
+
- Bash(git \*)
|
|
9
|
+
- Bash(gh \*)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Create One or More Pull Request from the Currently Modified or Staged Files
|
|
14
|
+
|
|
15
|
+
IMPORTANT: NON-INTERACTIVITY. This command should complete nearly always without human interaction. Only if you are really confused or lacking a major permission should you pause and ask question. Assume that the human is away from the keyboard while you are submitting their changes as PRs.
|
|
16
|
+
|
|
17
|
+
## Command's Purpose
|
|
18
|
+
|
|
19
|
+
The purpose of this command is to take all locally modified files (staged or not), split them into unrelated, or related but stackable commits and PRs, and do it non-interactively, and do it in such a away that both locally and on Github CI (if there is one) each PR passes, does not have merge conflicts, and should be ready to be reviewed and merged.
|
|
20
|
+
|
|
21
|
+
## Steps to Accomplish
|
|
22
|
+
|
|
23
|
+
1. First, feel free to `git add . ` so that all the modified files are staged. This command is deliberately mean to commit ALL modified files for simplicity, otherwise it's difficult to identify what should or should not be committed.
|
|
24
|
+
2. Second, if the repo supports command `just format` or `make format` or `rubocop -a` (or other safe linter fix commandss) — go ahead and run them now. If there are locally modified files, `git add` them.
|
|
25
|
+
3. If we are NOT on the main branch, then do a diff between the current branch and the main (excluding the staged files) to understand what the context of the current branch is.
|
|
26
|
+
4. Next you are going to review all the staged files and break them up by "context": i.e. that means they need to be committed and pushed to a PR together. This will also help you identify which files should be committed into the current branch (i.e. they all relate to the changes on this branch, or the branch name). If there are several unrelated changes in this set of staged files, OR multiple related changes, but there are a lot of changes (the diff is > 500 lines) you will need to break up this PR into multiple PRs.
|
|
27
|
+
|
|
28
|
+
Multiple PRs can either be on branches that are created off main (those changes should have nothing to do with the current branch), OR they can be committed and pushed as multiple PRs that are stacked on each other.
|
|
29
|
+
|
|
30
|
+
For every commit and branch that you push you must ensure that local tests are passing and if not either fix them (if that's easy) or refuse to push a PR until you pairing with the developer resolve all test issues.
|
|
31
|
+
|
|
32
|
+
Tests are typically invoked as:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
just test # if there is a justfile at the root
|
|
36
|
+
make test # if there is a Makefile at the root
|
|
37
|
+
bundle exec rspec --format documentation # if this is a ruby repo, meaning there is a Gemfile and Gemfile.lock at the root level and spec folder
|
|
38
|
+
npm run test # if there is package.json and a test command
|
|
39
|
+
# for other types of repos identify the most common ways to run tests, and do run them before committing any code.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Branch Names
|
|
43
|
+
|
|
44
|
+
We provided a convenient shell script in the .claude directory, called `./.claude/branch-name.sh`
|
|
45
|
+
|
|
46
|
+
To generate a new branch name, identify from three to six key words describing this change and then run from the project root:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
$ .claude/branch-name.sh add copilot LLM bubble for admins # this script will output the following
|
|
50
|
+
kig/add-copilot-llm-bubble-for-admins
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you run this script, capture it's STDOUT and you'll get the branch name.
|
|
54
|
+
|
|
55
|
+
It prefixes the current user `$USER` and a slash, and then dash-joins all the words you specified.
|
|
56
|
+
|
|
57
|
+
If you need information on how to split one large PR into smaller ones, the following URLs might be helpful:
|
|
58
|
+
|
|
59
|
+
- https://www.davepacheco.net/blog/2025/stacked-prs-on-github/
|
|
60
|
+
- https://itsnotbugitsfeature.com/2019/10/22/splitting-a-big-pull-request-into-smaller-review-able-ones
|
|
61
|
+
- https://newsletter.pragmaticengineer.com/p/stacked-diffs
|
|
62
|
+
|
|
63
|
+
## Committing Changes and Creating the PR
|
|
64
|
+
|
|
65
|
+
Once we are on the appropriate branch, and either there are only changes staged for commit or NO changes at all (so we'll be creating the PR from the branch).
|
|
66
|
+
|
|
67
|
+
## Rebase from main?
|
|
68
|
+
|
|
69
|
+
Compare the local main with remote main, and if they diverged, you'll want to stash all the changes, switch to main, do git pull, then restore the branch, and before you pop the stash, run `git rebase main`. Resolve any conflicts that you are able to do on your own, otherwise this is one case where you can engage interactively and ask the developer for help. Once the current branch is rebased, you `git stash pop`. This may also result in conflicts. The same thing: attempt to resolve them and commit the rebase, or engage with the developer.
|
|
70
|
+
|
|
71
|
+
### Committing Currently Staged Changes
|
|
72
|
+
|
|
73
|
+
At this point, you should either a clean branch with all changes committed, or some changes staged for commit.
|
|
74
|
+
|
|
75
|
+
If there are changes staged for commit, you must perform `git diff --cached`, and summarize those changes in the markdown format in a temporary file we'll call <tempfile> (preferably under /tmp folder), and create a short title for the commit.
|
|
76
|
+
|
|
77
|
+
Then we commit with `git commit -m "<title>" -F "<tempfile>"`, and push the changes `git push -u origin`.
|
|
78
|
+
|
|
79
|
+
### Creating the PR
|
|
80
|
+
|
|
81
|
+
This is the final step. You are to use command `gh` (which stands for `github`).
|
|
82
|
+
|
|
83
|
+
In order to create a PR we must NOT be on the `main` branch, we must have all changes pushed to the origin and we must have no locally untracked or modified files.
|
|
84
|
+
|
|
85
|
+
The next step is describing the PR.
|
|
86
|
+
|
|
87
|
+
You are going to perform the diff of the current branch with the remote `main` branch using `git diff origin/main...HEAD`, and analyze it. You will create a markdown file that we'll refer to <pr-description> preferably in the `/tmp` folder, that describes the changes of this PR precisely, professionally, without any emojis. It should have the top header title that we'll refer to <pr-title>, (with a single '#' in markdown) that will also be the name of the PR. It should have the sections such as Summary (a short abstract-type description, no more than a few paragraphs) Description (if necessary, a more detailed description), Motivation, Testing, Backwards Compatibility, Scalability & Performance Impact, and Code Quality Analysis.
|
|
88
|
+
|
|
89
|
+
Once completed analysis and summarizing of this change, you will invoke the command `gh pr create -a @me -B main -F "<pr-description>" -t "<pr-title>"`.
|
|
90
|
+
|
|
91
|
+
If the command responds with an error such as Github Token authorization is insufficient, you are to save the PR description in the file at the root of the project called `PR.md` and report this to the user. Also print the URL required to create the PR on the web by clicking on the URL.
|
|
92
|
+
|
|
93
|
+
If you were able to create the PR with the `gh pr create` command, then open the web page to it anyway. Get the PR ID with `gh pr list | grep "<pr-title>" | awk '{print $1}'`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
description: "Stash unstaged/untracked files while preserving staged changes"
|
|
4
|
+
allowed-tools:
|
|
5
|
+
|
|
6
|
+
- Bash(git \*)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Stash Unstaged Changes
|
|
11
|
+
|
|
12
|
+
This command stashes unstaged and untracked files while preserving any files already staged for commit.
|
|
13
|
+
|
|
14
|
+
Check if there are any files staged for commit (i.e. git added) with `git diff --cached`
|
|
15
|
+
|
|
16
|
+
- If there are none, simply run `git stash push --include-untracked`
|
|
17
|
+
- If there are unstaged changes in the workspace in addition to staged, you are to perform the following series of commands:
|
|
18
|
+
1. `git commit -m 'WIP: staged changes' --no-verify` — commit the staged changes temporarily
|
|
19
|
+
1. `git stash push --include-untracked` — to stash the remaining unstaged changes
|
|
20
|
+
1. `git reset --soft HEAD^` — to undo the temporary commit
|
|
21
|
+
1. `git add .` — to stage the previously staged for commit files
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
description: "Restore previously stashed unstaged changes"
|
|
4
|
+
allowed-tools:
|
|
5
|
+
|
|
6
|
+
- Bash(git \*)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Unstash Unstaged Changes
|
|
11
|
+
|
|
12
|
+
This command brings back the changes made by the "stash-unstaged" command.
|
|
13
|
+
|
|
14
|
+
Ensure that the current workspace is clean (no staged or unstaged files) and if there are any — ask if the user still wants to proceed.
|
|
15
|
+
If the user confirms, run `git stash apply` command to bring back previously stashed changes.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Edit",
|
|
5
|
+
"Bash(*)",
|
|
6
|
+
"Bash(chmod:*)",
|
|
7
|
+
"Skill(create-pr)",
|
|
8
|
+
"WebFetch(domain:docs.tabler.io)",
|
|
9
|
+
"WebFetch(domain:smartdatalake.ch)",
|
|
10
|
+
"WebSearch",
|
|
11
|
+
"WebFetch(*)",
|
|
12
|
+
"WebFetch(domain:github.com)",
|
|
13
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
14
|
+
"WebFetch(domain:www.vcpr.cz)",
|
|
15
|
+
"WebFetch(domain:renju.net)",
|
|
16
|
+
"WebFetch(domain:en.wikipedia.org)"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"enabledPlugins": {
|
|
20
|
+
"activerecord@claude-ruby-marketplace": true,
|
|
21
|
+
"build@motlin-claude-code-plugins": true,
|
|
22
|
+
"chrome-devtools-mcp@chrome-devtools-plugins": true,
|
|
23
|
+
"ci-cd@devops-skills": true,
|
|
24
|
+
"example-skills@anthropic-agent-skills": false,
|
|
25
|
+
"feature-dev@claude-code-plugins": true,
|
|
26
|
+
"frontend-design@claude-code-plugins": true,
|
|
27
|
+
"git@motlin-claude-code-plugins": true,
|
|
28
|
+
"iac-terraform@devops-skills": true,
|
|
29
|
+
"justfile@motlin-claude-code-plugins": true,
|
|
30
|
+
"pr-review-toolkit@claude-code-plugins": true,
|
|
31
|
+
"rspec@claude-ruby-marketplace": true,
|
|
32
|
+
"ruby-lsp@ruby-skills": true,
|
|
33
|
+
"ruby-skills@ruby-skills": true,
|
|
34
|
+
"security-guidance@claude-code-plugins": true,
|
|
35
|
+
"superpowers-developing-for-claude-code@superpowers-marketplace": true,
|
|
36
|
+
"superpowers@superpowers-marketplace": false
|
|
37
|
+
},
|
|
38
|
+
"extraKnownMarketplaces": {
|
|
39
|
+
"ruby-skills": {
|
|
40
|
+
"source": {
|
|
41
|
+
"source": "github",
|
|
42
|
+
"repo": "st0012/ruby-skills"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"claude-ruby-marketplace": {
|
|
46
|
+
"source": {
|
|
47
|
+
"source": "github",
|
|
48
|
+
"repo": "hoblin/claude-ruby-marketplace"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"devops-skills": {
|
|
52
|
+
"source": {
|
|
53
|
+
"source": "github",
|
|
54
|
+
"repo": "ahmedasmar/devops-claude-skills"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"motlin-claude-code-plugins": {
|
|
58
|
+
"source": {
|
|
59
|
+
"source": "github",
|
|
60
|
+
"repo": "motlin/claude-code-plugins"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"chrome-devtools-plugins": {
|
|
64
|
+
"source": {
|
|
65
|
+
"source": "github",
|
|
66
|
+
"repo": "ChromeDevTools/chrome-devtools-mcp"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"effortLevel": "high",
|
|
71
|
+
"skipDangerousModePermissionPrompt": true
|
|
72
|
+
}
|
data/.rubocop_todo.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This configuration was generated by
|
|
2
|
+
# `rubocop --auto-gen-config`
|
|
3
|
+
# on 2026-03-17 03:19:43 UTC using RuboCop version 1.85.1.
|
|
4
|
+
# The point is for the user to remove these configuration records
|
|
5
|
+
# one by one as the offenses are removed from the code base.
|
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
+
|
|
9
|
+
# Offense count: 2
|
|
10
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
11
|
+
Metrics/AbcSize:
|
|
12
|
+
Max: 20
|
|
13
|
+
|
|
14
|
+
# Offense count: 1
|
|
15
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
16
|
+
Metrics/CyclomaticComplexity:
|
|
17
|
+
Max: 9
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.1
|
data/CHANGELOG.md
ADDED
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# FlowEngine Rails
|
|
2
|
+
|
|
3
|
+
## What This Gem Does
|
|
4
|
+
|
|
5
|
+
`flowengine-rails` is a Rails Engine (v0.1.0) that wraps the `flowengine` core gem with ActiveRecord persistence, a Hotwire-based web wizard UI, an admin CRUD interface, and an iframe-embeddable widget. It lets non-technical users define multi-step form flows via a Ruby DSL, store them in the database, and serve them to end users as interactive step-by-step wizards.
|
|
6
|
+
|
|
7
|
+
The target application is **Qualified.at** -- a lead qualification service where flows are embedded in external client sites via iframes.
|
|
8
|
+
|
|
9
|
+
## Dependency on `flowengine` Core Gem
|
|
10
|
+
|
|
11
|
+
The core gem (`flowengine`, published on RubyGems as v0.4.0, source at `github.com/kigster/flowengine`) provides:
|
|
12
|
+
|
|
13
|
+
- **DSL** for defining flows: `FlowEngine.define { ... }` with steps, transitions, rules
|
|
14
|
+
- **Engine** (state machine): `FlowEngine::Engine` drives step traversal, answers, history
|
|
15
|
+
- **Node** objects representing individual steps with types (`:text`, `:boolean`, `:number`, `:single_select`, `:multi_select`, `:number_matrix`, `:display`)
|
|
16
|
+
- **Graph/MermaidExporter** for diagram visualization
|
|
17
|
+
- **Rules/Evaluator** for conditional transitions (`if_rule:`, `contains()`, etc.)
|
|
18
|
+
- **Validation** for DSL correctness
|
|
19
|
+
|
|
20
|
+
This gem calls into `flowengine` via:
|
|
21
|
+
- `FlowEngine.load_dsl(dsl_text)` -- parses DSL string into a Definition
|
|
22
|
+
- `FlowEngine::Engine.new(definition)` / `FlowEngine::Engine.from_state(definition, state)` -- creates/restores engine
|
|
23
|
+
- `engine.answer(value)`, `engine.to_state`, `engine.finished?` -- drives flow
|
|
24
|
+
- `definition.step_ids`, `definition.step(id)` -- introspects steps
|
|
25
|
+
- `FlowEngine::Graph::MermaidExporter` -- generates Mermaid diagrams
|
|
26
|
+
|
|
27
|
+
**Local development**: `Gemfile` uses `path: "../flowengine"` (sibling directory). **CI/branches**: changed to `github: "kigster/flowengine"`. The gemspec declares `spec.add_dependency "flowengine", "~> 0.1"`.
|
|
28
|
+
|
|
29
|
+
## Repository Layout
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
flowengine-rails.gemspec # Gem metadata, deps: flowengine ~> 0.1, rails >= 8.0.1, stimulus-rails, turbo-rails
|
|
33
|
+
Gemfile # Dev deps: rspec-rails, rspec-its, rubocop, capybara, simplecov, sqlite3
|
|
34
|
+
Rakefile # Default task: spec + rubocop
|
|
35
|
+
|
|
36
|
+
lib/
|
|
37
|
+
flowengine/rails.rb # Entry point, Configuration singleton, Error class
|
|
38
|
+
flowengine/rails/version.rb # VERSION = "0.1.0"
|
|
39
|
+
flowengine/rails/configuration.rb # embed_allowed_origins, layouts, cache, callbacks, admin auth
|
|
40
|
+
flowengine/rails/dsl_loader.rb # Thread-safe in-memory cache around FlowEngine.load_dsl
|
|
41
|
+
flowengine/rails/engine.rb # Rails::Engine with asset paths, importmap, generator config
|
|
42
|
+
generators/
|
|
43
|
+
flow_engine/install/ # rails g flow_engine:install -- migrations, initializer, route mount
|
|
44
|
+
flow_engine/flow/ # rails g flow_engine:flow NAME -- definition file + seed rake task
|
|
45
|
+
|
|
46
|
+
app/
|
|
47
|
+
models/flow_engine/
|
|
48
|
+
application_record.rb # Base AR class
|
|
49
|
+
flow_definition.rb # name, version, dsl (text), active (boolean). Validates DSL parses.
|
|
50
|
+
# Auto-increments version on create. activate!/deactivate! toggles.
|
|
51
|
+
# readonly? when sessions exist. Generates mermaid diagrams.
|
|
52
|
+
flow_session.rb # belongs_to definition. Tracks current_step_id, answers (JSON),
|
|
53
|
+
# history (JSON), status (in_progress/completed/abandoned).
|
|
54
|
+
# advance!(answer) drives the engine. fire_completion_callback on complete.
|
|
55
|
+
controllers/flow_engine/
|
|
56
|
+
application_controller.rb # Embed mode detection (?embed=true), CORS headers, layout switching
|
|
57
|
+
sessions_controller.rb # new, create, show, update, completed, abandon
|
|
58
|
+
# Parses answer by step type (multi_select->array, number->int, etc.)
|
|
59
|
+
admin/definitions_controller.rb # Full CRUD + activate/deactivate/mermaid. Admin auth via config.
|
|
60
|
+
helpers/flow_engine/
|
|
61
|
+
sessions_helper.rb # render_step(node, form) dispatches to type-specific partials
|
|
62
|
+
views/
|
|
63
|
+
flow_engine/sessions/ # new, show, completed templates
|
|
64
|
+
flow_engine/sessions/steps/ # Partials: _boolean, _text, _number, _single_select,
|
|
65
|
+
# _multi_select, _number_matrix, _display, _unknown
|
|
66
|
+
flow_engine/admin/definitions/ # index, show, new, edit, _form, mermaid
|
|
67
|
+
layouts/flow_engine/ # application.html.erb (standalone), embed.html.erb (iframe)
|
|
68
|
+
assets/
|
|
69
|
+
javascripts/flow_engine/ # embed.js (iframe resizer), progress_controller.js, step_controller.js
|
|
70
|
+
stylesheets/flow_engine/ # application.css (full standalone stylesheet)
|
|
71
|
+
|
|
72
|
+
config/routes.rb # sessions (new/create/show/update + completed/abandon)
|
|
73
|
+
# admin/definitions (CRUD + activate/deactivate/mermaid)
|
|
74
|
+
# root -> sessions#new
|
|
75
|
+
|
|
76
|
+
db/migrate/
|
|
77
|
+
01_create_flow_engine_definitions.rb # name, version, dsl, active. Unique index on [name, version].
|
|
78
|
+
02_create_flow_engine_sessions.rb # definition_id (FK), current_step_id, answers/history/metadata (JSON), status
|
|
79
|
+
|
|
80
|
+
spec/
|
|
81
|
+
spec_helper.rb # SQLite in-memory, schema created inline, SIMPLE_DSL fixture
|
|
82
|
+
dummy/ # Minimal Rails 8 app (SQLite :memory:, engine mounted at /flow_engine)
|
|
83
|
+
controllers/ # Request specs for sessions + admin/definitions
|
|
84
|
+
models/ # Unit specs for FlowDefinition, FlowSession
|
|
85
|
+
lib/ # Specs for Configuration, DslLoader
|
|
86
|
+
routing/ # Route specs
|
|
87
|
+
|
|
88
|
+
.github/workflows/
|
|
89
|
+
main.yml # Ruby 3.4.4, bundle exec rake (spec + rubocop)
|
|
90
|
+
rspec.yml # Ruby 4.0, checks out kigster/flowengine alongside, bundle exec rspec
|
|
91
|
+
rubocop.yml # Ruby 4.0, bundle exec rubocop
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Database Schema
|
|
95
|
+
|
|
96
|
+
**flow_engine_definitions**: `name` (string), `version` (integer, auto-incremented per name), `dsl` (text, Ruby DSL source), `active` (boolean). Unique index on `[name, version]`.
|
|
97
|
+
|
|
98
|
+
**flow_engine_sessions**: `definition_id` (FK), `current_step_id` (string), `answers` (JSON), `history` (JSON, array of visited step IDs), `status` (string: in_progress/completed/abandoned), `metadata` (JSON).
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
FlowEngine::Rails.configure do |config|
|
|
104
|
+
config.embed_allowed_origins = ["https://example.com"] # CORS for iframe embed
|
|
105
|
+
config.default_layout = "flow_engine/application" # Standalone layout
|
|
106
|
+
config.embed_layout = "flow_engine/embed" # Iframe layout
|
|
107
|
+
config.cache_definitions = true # In-memory DSL parse cache
|
|
108
|
+
config.on_session_complete = ->(session) { ... } # Completion callback
|
|
109
|
+
config.admin_authentication_method = :authenticate_admin! # Admin before_action
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Step Types and Answer Parsing
|
|
114
|
+
|
|
115
|
+
| Step type | View partial | Answer param | Parsed as |
|
|
116
|
+
|-----------------|--------------------|-----------------------|----------------------|
|
|
117
|
+
| `:text` | `_text` | `params[:answer]` | String |
|
|
118
|
+
| `:number` | `_number` | `params[:answer]` | Integer (`.to_i`) |
|
|
119
|
+
| `:boolean` | `_boolean` | `params[:answer]` | Boolean (`== "true"`)|
|
|
120
|
+
| `:single_select`| `_single_select` | `params[:answer]` | String |
|
|
121
|
+
| `:multi_select` | `_multi_select` | `params[:answer_values]` | Array |
|
|
122
|
+
| `:number_matrix`| `_number_matrix` | `params[:answer_fields]` | Hash (string->int)|
|
|
123
|
+
| `:display` | `_display` | (none, info-only) | N/A |
|
|
124
|
+
|
|
125
|
+
## Running Tests and Linting
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
bundle exec rspec # Tests (SQLite in-memory, no external DB needed)
|
|
129
|
+
bundle exec rubocop # Lint
|
|
130
|
+
bundle exec rake # Both (default task)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## CI Notes
|
|
134
|
+
|
|
135
|
+
- **Gemfile.lock must include `x86_64-linux` platform** (`bundle lock --add-platform x86_64-linux`)
|
|
136
|
+
- The `rspec.yml` workflow checks out `kigster/flowengine` as a sibling directory for the path dependency
|
|
137
|
+
- The `main.yml` workflow runs on Ruby 3.4.4; `rspec.yml` and `rubocop.yml` run on Ruby 4.0
|
|
138
|
+
- Required Ruby version: `>= 4.0.1` (gemspec)
|
|
139
|
+
|
|
140
|
+
## RuboCop Configuration
|
|
141
|
+
|
|
142
|
+
- Target Ruby 4.0, double quotes enforced, frozen string literal required
|
|
143
|
+
- Line length max 120, method length max 20
|
|
144
|
+
- `spec/dummy/` excluded from cops
|
|
145
|
+
- `inherit_from: .rubocop_todo.yml`
|
|
146
|
+
|
|
147
|
+
## Key Architectural Patterns
|
|
148
|
+
|
|
149
|
+
- **Isolated namespace**: all code under `FlowEngine` module, engine uses `isolate_namespace FlowEngine`
|
|
150
|
+
- **Immutable definitions**: once a `FlowDefinition` has sessions, it becomes `readonly?`; users must create a new version
|
|
151
|
+
- **State reconstruction**: `FlowSession#engine` rebuilds `FlowEngine::Engine` from persisted state on every request (stateless controller pattern)
|
|
152
|
+
- **Embed mode**: `?embed=true` query param switches layout and enables CORS headers
|
|
153
|
+
- **Thread-safe caching**: `DslLoader` uses `Mutex` for DSL parse cache
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Konstantin Gredeskoul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|