rails-mmd 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/CHANGELOG.md +10 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/SECURITY.md +40 -0
- data/docs/active-record-association-support.md +37 -0
- data/docs/assets/branding/rails-mmd-social-preview.jpg +0 -0
- data/docs/usage.md +126 -0
- data/exe/rails-mmd +6 -0
- data/fixtures/schemas/diagnostics/valid/catalog.json +39 -0
- data/lib/rails_mmd/artifact_ref_sanitizer.rb +59 -0
- data/lib/rails_mmd/attribute_types.rb +29 -0
- data/lib/rails_mmd/canonical_json.rb +19 -0
- data/lib/rails_mmd/cli.rb +86 -0
- data/lib/rails_mmd/config.rb +249 -0
- data/lib/rails_mmd/constant_resolver.rb +33 -0
- data/lib/rails_mmd/diagnostic_factory.rb +122 -0
- data/lib/rails_mmd/domain_resolver.rb +97 -0
- data/lib/rails_mmd/exit_policy.rb +27 -0
- data/lib/rails_mmd/generate_command.rb +116 -0
- data/lib/rails_mmd/generation_pipeline.rb +109 -0
- data/lib/rails_mmd/ir_builder.rb +122 -0
- data/lib/rails_mmd/mermaid_serializer.rb +168 -0
- data/lib/rails_mmd/metadata_shapes.rb +55 -0
- data/lib/rails_mmd/model_inventory.rb +144 -0
- data/lib/rails_mmd/ordering.rb +25 -0
- data/lib/rails_mmd/output_directory.rb +192 -0
- data/lib/rails_mmd/publisher.rb +166 -0
- data/lib/rails_mmd/rails_loader.rb +96 -0
- data/lib/rails_mmd/redactor.rb +143 -0
- data/lib/rails_mmd/relationship_builder.rb +982 -0
- data/lib/rails_mmd/render_plan_builder.rb +192 -0
- data/lib/rails_mmd/safe_token_assigner.rb +130 -0
- data/lib/rails_mmd/schema_probe.rb +376 -0
- data/lib/rails_mmd/schema_validator.rb +136 -0
- data/lib/rails_mmd/subject_ids.rb +17 -0
- data/lib/rails_mmd/tooling_tasks.rb +47 -0
- data/lib/rails_mmd/verification_runner.rb +143 -0
- data/lib/rails_mmd/version.rb +5 -0
- data/lib/rails_mmd.rb +15 -0
- data/schemas/config.schema.json +54 -0
- data/schemas/diagnostics.schema.json +418 -0
- data/schemas/ir.schema.json +84 -0
- data/schemas/render_plan.schema.json +136 -0
- metadata +135 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2439ad7662a9d94eefa0adc2a8805699f5fb606a5a336d136c99f5d0586f8d7a
|
|
4
|
+
data.tar.gz: 8d87bf76c08dcad2af3d27f05aed3c4edd9067d344f04760e5d1778bc30f295b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: eb2d72e44b51ce0ff58748a3d2be4cafba64f1619cb8886871d417343431bcfcdf6f8b20c2bf035f0dec2a515b016c117bab3dbbe496eecbefc9ffca8d0f3652
|
|
7
|
+
data.tar.gz: fbcce4fff00660bf1049ee7e04097ea05a315c4169834b746084302314c0c004c978033e6e3082598074789596a0503dbe321e00442ac7503143958aa3d1abef
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Generate Mermaid ER and class diagrams from explicitly configured Rails model domains.
|
|
8
|
+
- Include primary and foreign key attributes and supported direct, through, polymorphic, and HABTM associations. See the [support matrix](docs/active-record-association-support.md) for limits.
|
|
9
|
+
- Write validated render plans and structured diagnostics alongside diagrams, with deterministic output and atomic file replacement.
|
|
10
|
+
- Verify Rails 7.2 with Ruby 4.0.6, and Rails 8.1 with Ruby 3.3.12 and 4.0.6, using real SQLite fixture applications.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AudioStakes
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# rails-mmd
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Generate Mermaid ER and class diagrams from the models and database schema of a Rails application. `rails-mmd` runs in the application's bundle, loads its models, and writes local `.mmd` source files. Preview them with a Mermaid viewer or embed their contents in a Markdown `mermaid` code block.
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
erDiagram
|
|
9
|
+
direction LR
|
|
10
|
+
USER }o..|| ACCOUNT : account
|
|
11
|
+
USER {
|
|
12
|
+
bigint id PK
|
|
13
|
+
bigint account_id FK
|
|
14
|
+
}
|
|
15
|
+
ACCOUNT {
|
|
16
|
+
bigint id PK
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The diagram above is illustrative. The actual output depends on your models, associations, and database metadata.
|
|
21
|
+
|
|
22
|
+
## Try it in a Rails app
|
|
23
|
+
|
|
24
|
+
You need an existing Rails application with Active Record models and a prepared database. Check the [tested Ruby/Rails combinations](#compatibility-and-limits) below before installing. All commands in this section run from that application's root, not this repository.
|
|
25
|
+
|
|
26
|
+
In the **target Rails application's** `Gemfile`, add the published [RubyGems package](https://rubygems.org/gems/rails-mmd):
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem 'rails-mmd', '~> 0.1.0', require: false
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run `bundle install`. The application's `Gemfile.lock` records the selected release. At the application root, create `rails_mmd.yml` and replace the model names with real models in your app:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
version: 1
|
|
36
|
+
domains:
|
|
37
|
+
core:
|
|
38
|
+
include_models:
|
|
39
|
+
- User
|
|
40
|
+
- Post
|
|
41
|
+
output:
|
|
42
|
+
directory: tmp/rails_mmd
|
|
43
|
+
format: both
|
|
44
|
+
attributes: keys
|
|
45
|
+
direction: LR
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then run:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
RAILS_ENV=development bundle exec rails-mmd generate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Inspect `tmp/rails_mmd/core.er.mmd` and `tmp/rails_mmd/core.class.mmd`. A successful run can be silent; these generated files are the result. To view the diagrams, follow [Viewing a generated diagram](docs/usage.md#viewing-a-generated-diagram). The selected environment must be able to boot and read its database schema. The command eager-loads the Rails application; run it only with an application and configuration you trust.
|
|
55
|
+
|
|
56
|
+
The configuration selects models explicitly for each domain. Relationships are drawn between supported models selected in the same domain. To see all options, output files, diagnostics, and exit codes, read [the usage guide](docs/usage.md).
|
|
57
|
+
|
|
58
|
+
## Compatibility and limits
|
|
59
|
+
|
|
60
|
+
The gem declares Ruby `>= 3.3, < 4.1`. The local real-app matrix verifies Rails 7.2 on Ruby 4.0.6, and Rails 8.1 on Ruby 3.3.12 and 4.0.6. Other combinations within the Ruby range have not been verified by that matrix. The real-app matrix uses SQLite; PostgreSQL and MySQL have not been verified in a real-app run.
|
|
61
|
+
|
|
62
|
+
Supported associations and current limits are listed in the [Active Record support matrix](docs/active-record-association-support.md). In particular, a domain spanning multiple database connection contexts is unsupported, as are composite keys and several association options. Check diagnostics when a diagram omits a relationship.
|
|
63
|
+
|
|
64
|
+
Generated diagrams and JSON artifacts can reveal model names, table names, columns, relationships, and connection identifiers in diagnostics. Review them before committing or sharing them. Output must stay inside the application's root; for private schemas, add the output directory to the application's `.gitignore` (for this example, `/tmp/rails_mmd/`).
|
|
65
|
+
|
|
66
|
+
See the [security policy](https://github.com/AudioStakes/rails-mmd/blob/main/SECURITY.md) for the Rails boot trust boundary and vulnerability reporting.
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
The [generation pipeline](https://github.com/AudioStakes/rails-mmd/blob/main/lib/rails_mmd/generation_pipeline.rb) separates model inventory, domain selection, schema probing, relationship construction, and rendering. It converts Rails metadata into an intermediate representation and validated render plans; Mermaid serialization consumes those plans without querying Rails or the database.
|
|
71
|
+
|
|
72
|
+
Relationship cardinality uses database foreign-key, nullability, and unique-index evidence. Structured diagnostics explain omitted unsupported associations and degraded metadata. The [pipeline specs](https://github.com/AudioStakes/rails-mmd/blob/main/spec/rails_mmd/generation_pipeline_spec.rb) exercise these stages together, and the [publisher specs](https://github.com/AudioStakes/rails-mmd/blob/main/spec/rails_mmd/publisher_spec.rb) cover validation, selected-domain output, and rollback when file replacement fails. The [real-app matrix](https://github.com/AudioStakes/rails-mmd/blob/main/docs/development/ci.md) checks the public command against the declared Ruby/Rails pairs.
|
|
73
|
+
|
|
74
|
+
## Contributing and development
|
|
75
|
+
|
|
76
|
+
See [CONTRIBUTING.md](https://github.com/AudioStakes/rails-mmd/blob/main/CONTRIBUTING.md) for local setup and checks, the [changelog](https://github.com/AudioStakes/rails-mmd/blob/main/CHANGELOG.md) for released changes, and the [release guide](https://github.com/AudioStakes/rails-mmd/blob/main/docs/development/releasing.md) for the manual publishing process. Implementation contracts are in [docs/p0-contract.md](https://github.com/AudioStakes/rails-mmd/blob/main/docs/p0-contract.md); the [development notes](https://github.com/AudioStakes/rails-mmd/blob/main/docs/development/tooling.md) cover the Rake tasks. The contributor guide and development notes are maintained in the repository, outside the installed gem. Serena is optional local development tooling, not a runtime dependency.
|
|
77
|
+
|
|
78
|
+
The [P0 blocking fixture matrix](https://github.com/AudioStakes/rails-mmd/blob/main/docs/p0-blocking-fixture-matrix.md) records the required verification cases.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT. See [LICENSE](LICENSE).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please do not put credentials, private application code, database contents, or
|
|
6
|
+
exploit details in a public issue or pull request.
|
|
7
|
+
|
|
8
|
+
Use [GitHub private vulnerability reporting](https://github.com/AudioStakes/rails-mmd/security/advisories/new)
|
|
9
|
+
to send a suspected vulnerability to the maintainer privately.
|
|
10
|
+
|
|
11
|
+
If the private form is unavailable, open an
|
|
12
|
+
[issue](https://github.com/AudioStakes/rails-mmd/issues/new) asking the maintainer
|
|
13
|
+
for a private reporting channel. Include only that request, with no sensitive
|
|
14
|
+
details, and wait for a private channel before sharing the report.
|
|
15
|
+
|
|
16
|
+
Once a private channel is established, include the affected commit or version,
|
|
17
|
+
Ruby and Rails versions, expected and actual behavior, and a minimal reproduction
|
|
18
|
+
using synthetic data. There is no guaranteed response time or published security
|
|
19
|
+
backport policy yet.
|
|
20
|
+
|
|
21
|
+
## Running rails-mmd safely
|
|
22
|
+
|
|
23
|
+
`rails-mmd generate` loads the target application's `config/environment.rb` and
|
|
24
|
+
eager-loads its Ruby code in the current process. Initializers and application
|
|
25
|
+
code run with your operating-system permissions, environment, and database
|
|
26
|
+
credentials. Use only applications and configuration you trust. Prefer a local
|
|
27
|
+
development environment with a disposable database; application startup can have
|
|
28
|
+
side effects even though rails-mmd inspects schema metadata rather than records.
|
|
29
|
+
|
|
30
|
+
The tool writes artifacts under the application directory and does not upload
|
|
31
|
+
them. Application startup code may still contact services. The YAML configuration
|
|
32
|
+
uses safe loading, and output paths are checked against the application root;
|
|
33
|
+
these checks do not sandbox application code.
|
|
34
|
+
|
|
35
|
+
Generated diagrams and JSON artifacts contain model, table, column, and
|
|
36
|
+
relationship names. Treat them as application documentation that may be
|
|
37
|
+
confidential. Review artifacts and console output before sharing them or pasting
|
|
38
|
+
them into an online Mermaid viewer. Diagnostic redaction is a limited safeguard,
|
|
39
|
+
not a guarantee that every secret is removed; application logs are outside its
|
|
40
|
+
control.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Active Record association support (Rails 7.2 / 8.1)
|
|
2
|
+
|
|
3
|
+
The target Rails series are 7.2 and 8.1. "Supported" describes behavior covered by the current implementation and tests; it does not imply support for every option of that association. The real-app matrix currently uses SQLite. PostgreSQL and MySQL have not been verified in a real-app run.
|
|
4
|
+
|
|
5
|
+
## Implemented features
|
|
6
|
+
|
|
7
|
+
| Priority | Active Record feature | Status | Implemented behavior |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| P1-01 | Rails 7.2 / 8.1 runtime compatibility | Supported | Three Ruby/Rails pairs run through the local real-app matrix. |
|
|
10
|
+
| P1-02 | Association macro detection | Supported | Reflections are classified; unsupported macros produce diagnostics. |
|
|
11
|
+
| P1-03 | Direct `has_many` / `has_one` | Supported | Direct associations and self joins have verified cardinalities. |
|
|
12
|
+
| P1-04 | `inverse_of` | Supported | Physical relationships are merged into a canonical edge independently of an inverse declaration. |
|
|
13
|
+
| P1-05 | `has_many :through` / `has_one :through` | Supported | Inferred-source one-step and nested paths are rendered as semantic edges without duplicate endpoints. |
|
|
14
|
+
| P1-06 | Polymorphic associations | Supported | Selected matching inverse candidates produce one normalized edge per concrete target. |
|
|
15
|
+
| P1-07 | `has_and_belongs_to_many` | Supported | A hidden join table is verified and rendered as one normalized many-to-many relationship. |
|
|
16
|
+
|
|
17
|
+
## Not yet supported
|
|
18
|
+
|
|
19
|
+
| Priority | Active Record feature | Needed for support |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| P2-01 | Scoped associations | Retain scope presence as metadata. |
|
|
22
|
+
| P2-02 | STI | Define how base and derived classes appear. |
|
|
23
|
+
| P2-03 | `delegated_type` | Render the delegator and concrete types. |
|
|
24
|
+
| P2-04 | Composite primary/foreign keys and `query_constraints` | Preserve and match key tuples. |
|
|
25
|
+
| P2-05 | Non-default `primary_key`, `source`, `source_type`, and `as` options | Resolve the target and key for each option. The supported polymorphic inverse case already uses `as:`. |
|
|
26
|
+
| P2-06 | Cross-domain and multi-database relationships | Represent external nodes or emit diagnostics. |
|
|
27
|
+
| P2-07 | `dependent`, `touch`, and `counter_cache` | Decide which behavior belongs in the diagram metadata. |
|
|
28
|
+
| P3-01 | `disable_joins`, `strict_loading`, and async | Decide whether execution properties belong in the diagram. |
|
|
29
|
+
| P3-02 | Association extensions | Represent or diagnose structural extensions. |
|
|
30
|
+
|
|
31
|
+
The supported baseline covers non-polymorphic, unscoped, scalar direct `belongs_to`; scalar `foreign_key` and `class_name`; conservative cardinalities from foreign keys, nullability, and unique indexes; same-domain ER/class output; diagnostics for excluded supported `belongs_to`; direct polymorphic `belongs_to` with selected `has_many` / `has_one ..., as:` inverse candidates; and unscoped scalar HABTM with a verified hidden join table.
|
|
32
|
+
|
|
33
|
+
## References
|
|
34
|
+
|
|
35
|
+
- [P0 contract](https://github.com/AudioStakes/rails-mmd/blob/main/docs/p0-contract.md)
|
|
36
|
+
- [Relationship builder](../lib/rails_mmd/relationship_builder.rb) and [render plan builder](../lib/rails_mmd/render_plan_builder.rb)
|
|
37
|
+
- [Rails Association Basics](https://guides.rubyonrails.org/association_basics.html), [Rails 7.2 API](https://api.rubyonrails.org/v7.2/classes/ActiveRecord/Associations/ClassMethods.html), [Rails 8.1 API](https://api.rubyonrails.org/v8.1/classes/ActiveRecord/Associations/ClassMethods.html)
|
|
Binary file
|
data/docs/usage.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# rails-mmd Usage
|
|
2
|
+
|
|
3
|
+
Run `rails-mmd generate` from the **target Rails application's root**, with `rails-mmd` in that application's `Gemfile` (see the [quick start](../README.md)). The command loads and eager-loads the Rails app, reads metadata for selected models, builds Mermaid render plans, and publishes local artifacts. Use an environment that can boot the app and read its database schema.
|
|
4
|
+
|
|
5
|
+
Booting also executes the application's initializers. Use an application and environment you trust; the [security policy](../SECURITY.md) describes this boundary and how to handle generated schema information.
|
|
6
|
+
|
|
7
|
+
## Command
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
RAILS_ENV=development bundle exec rails-mmd generate \
|
|
11
|
+
--config rails_mmd.yml \
|
|
12
|
+
--output-dir tmp/rails_mmd \
|
|
13
|
+
--domain core \
|
|
14
|
+
--format both \
|
|
15
|
+
--fail-on-warning
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`--config` is optional as an option, but the resolved configuration file must exist. Paths for output are relative to the application root; the output directory must stay within it. `--config` may be an absolute path.
|
|
19
|
+
|
|
20
|
+
| Option | Default | Meaning |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `--config PATH` | `rails_mmd.yml` | Configuration file to load |
|
|
23
|
+
| `--output-dir DIR` | Config value, then `docs/rails_mmd` | Application-root-relative artifact directory |
|
|
24
|
+
| `--domain ID` | All configured domains | Render one configured domain |
|
|
25
|
+
| `--format er\|class\|both` | Config value, then `both` | Artifact formats to publish |
|
|
26
|
+
| `--fail-on-warning` | False | Return exit `1` when warning diagnostics are present |
|
|
27
|
+
|
|
28
|
+
## Minimal configuration
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
version: 1
|
|
32
|
+
domains:
|
|
33
|
+
core:
|
|
34
|
+
include_models:
|
|
35
|
+
- User
|
|
36
|
+
exclude_models: []
|
|
37
|
+
output:
|
|
38
|
+
directory: tmp/rails_mmd
|
|
39
|
+
format: both
|
|
40
|
+
attributes: keys
|
|
41
|
+
direction: LR
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Replace `User` with a named Active Record model in your app. `include_models` is required and must be nonempty. A model must be loaded and have usable table metadata. The command does not search for models automatically or install Mermaid CLI.
|
|
45
|
+
|
|
46
|
+
## Configuration reference
|
|
47
|
+
|
|
48
|
+
Unknown keys are rejected. The [configuration schema](../schemas/config.schema.json) is the machine-readable reference.
|
|
49
|
+
|
|
50
|
+
| Key | Default | Allowed values / meaning |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `version` | Required | Integer `1` |
|
|
53
|
+
| `domains` | Required | One or more named model groups |
|
|
54
|
+
| `domains.<id>.include_models` | Required | Nonempty list of exact Ruby constant names, such as `User` or `Billing::Invoice` |
|
|
55
|
+
| `domains.<id>.exclude_models` | `[]` | Exact model names to remove from that domain's included set |
|
|
56
|
+
| `output.directory` | `docs/rails_mmd` | Relative path inside the application root |
|
|
57
|
+
| `output.format` | `both` | `er`, `class`, or `both` |
|
|
58
|
+
| `output.attributes` | `keys` | `keys` shows primary and foreign keys; `none` hides attributes |
|
|
59
|
+
| `output.direction` | `LR` | `LR` left to right, `RL` right to left, `TB` top to bottom, `BT` bottom to top |
|
|
60
|
+
|
|
61
|
+
Domain IDs use lowercase letters and digits, start with a letter, and may contain single underscores between nonempty segments: `core` and `billing_v2` are valid; `Core`, `billing-v2`, and `billing__v2` are not. Model names are case-sensitive; wildcards and regular expressions are not supported. Each domain renders independently. Include both endpoints of a relationship in the same domain to display it.
|
|
62
|
+
|
|
63
|
+
CLI values for `--output-dir` and `--format` override their YAML equivalents. `--domain` selects one configured domain; omitting it renders all domains. `attributes` and `direction` are YAML settings only.
|
|
64
|
+
|
|
65
|
+
## Viewing a generated diagram
|
|
66
|
+
|
|
67
|
+
The `.mmd` files contain Mermaid source, not an image or an HTML page. Open them in a local viewer with Mermaid support, or copy the contents of one file into a Markdown file between these fences:
|
|
68
|
+
|
|
69
|
+
````markdown
|
|
70
|
+
```mermaid
|
|
71
|
+
<paste the contents of core.er.mmd here, replacing this line>
|
|
72
|
+
```
|
|
73
|
+
````
|
|
74
|
+
|
|
75
|
+
Open that Markdown file in a viewer that renders Mermaid code blocks. An ordinary text editor displays the source only. The illustrative diagram in the [README](../README.md) demonstrates this embedding. Keep private application diagrams in a local viewer; copying them to a hosted editor shares their schema content with that service.
|
|
76
|
+
|
|
77
|
+
## Artifacts
|
|
78
|
+
|
|
79
|
+
Successful domains can publish:
|
|
80
|
+
|
|
81
|
+
- `<domain>.diagnostics.json`
|
|
82
|
+
- `<domain>.er.mmd`
|
|
83
|
+
- `<domain>.er.render_plan.json`
|
|
84
|
+
- `<domain>.class.mmd`
|
|
85
|
+
- `<domain>.class.render_plan.json`
|
|
86
|
+
|
|
87
|
+
`global.diagnostics.json` is published only for post-output invocation-scope diagnostics when applicable. Empty diagnostics files are omitted. A run replaces the managed artifacts for selected domains in the output directory; keep unrelated files separate from these names.
|
|
88
|
+
|
|
89
|
+
Blocking diagnostics usually publish diagnostics JSON when output is available and omit the affected Mermaid and render-plan artifacts. Publish failures may fall back to sanitized stderr and leave previous managed artifacts intact. Pre-output fatal failures write sanitized diagnostics to stderr and write no files.
|
|
90
|
+
|
|
91
|
+
The artifacts include application schema information and can include connection identifiers in diagnostics. Review them before sharing or committing them; use a private or ignored output directory for private schemas.
|
|
92
|
+
|
|
93
|
+
## Exit codes
|
|
94
|
+
|
|
95
|
+
| Exit | Meaning |
|
|
96
|
+
|---:|---|
|
|
97
|
+
| 0 | success, including warning diagnostics when `--fail-on-warning` is absent |
|
|
98
|
+
| 1 | warning diagnostics were present and `--fail-on-warning` was set |
|
|
99
|
+
| 2 | config, output setup, Rails boot, domain, or schema probe error |
|
|
100
|
+
| 3 | Mermaid serialization failure or unresolved safe-token collision |
|
|
101
|
+
| 4 | output write or atomic publish failure |
|
|
102
|
+
| 99 | internal error |
|
|
103
|
+
|
|
104
|
+
CLI argument errors also return exit `1` before generation starts. These include unknown options, unexpected positional arguments, missing option values, and invalid boolean values. Check stderr for the argument error.
|
|
105
|
+
|
|
106
|
+
If the command reports a Rails load failure, check that you ran it from the application's root, in its bundle, and that the selected Rails environment can boot. If a model is missing, check its constant name and whether it was loaded by Rails. Domain and association limits are described in the [support matrix](active-record-association-support.md).
|
|
107
|
+
|
|
108
|
+
## Troubleshooting
|
|
109
|
+
|
|
110
|
+
Start with `bundle exec rails-mmd generate --help` to verify that the executable is available in the application's bundle. After generation, inspect `<domain>.diagnostics.json` in the output directory, along with stderr. A warning can omit an unsupported relationship while still returning exit `0`; use `--fail-on-warning` when omissions should fail a check.
|
|
111
|
+
|
|
112
|
+
| Symptom | What to check |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Bundler cannot find `rails-mmd` | Add the gem to the target application's Gemfile and run `bundle install` there. |
|
|
115
|
+
| Unknown option, unexpected argument, or invalid boolean | Compare the command with `bundle exec rails-mmd generate --help`. Pass model names through YAML, not positional arguments. Use `--fail-on-warning` as a flag, or set it explicitly to `true` or `false`. |
|
|
116
|
+
| Configuration file not found or invalid | Create `rails_mmd.yml` in the application root; compare keys and values with the configuration reference above. |
|
|
117
|
+
| Rails boot or database metadata error | Use the application's usual setup instructions to prepare its dependencies, environment, and database. Run `RAILS_ENV=development bundle exec rails runner 'puts Rails.env'` to isolate boot failures. |
|
|
118
|
+
| Model not found | Use its full constant name, including namespace, and ensure Rails eager loading can load it. |
|
|
119
|
+
| Expected relationship is missing | Include both models in the same domain, inspect warning diagnostics, and check the support matrix for unsupported options. |
|
|
120
|
+
| Output directory rejected | Use an application-root-relative path such as `tmp/rails_mmd`; paths escaping the root are rejected. |
|
|
121
|
+
| Output write or publish failure | Check stderr and inspect the output directory before retrying; follow the recovery steps below. |
|
|
122
|
+
| Diagram appears as text | Use a Mermaid-capable viewer or the Markdown embedding instructions above. |
|
|
123
|
+
|
|
124
|
+
If publication and rollback both fail, originals that could not be restored remain in a hidden `.rails-mmd-backup-*` directory inside the output directory. Inspect those backups first. Correct the filesystem problem, such as permissions or insufficient disk space, then manually restore the originals you need and verify them before running generation again. Keep the backups until recovery is confirmed.
|
|
125
|
+
|
|
126
|
+
`rails-mmd generate` does not invoke Mermaid CLI or any hosted service.
|
data/exe/rails-mmd
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"scope": "domain",
|
|
4
|
+
"domain_id": "core",
|
|
5
|
+
"diagnostics": [
|
|
6
|
+
{ "diagnostic_id": "d_config_not_found", "code": "CONFIG_NOT_FOUND", "severity": "error", "phase": "config", "scope": "invocation", "subject_id": null, "message": "config missing", "metadata": { "config_path": "rails_mmd.yml" }, "artifact_refs": [], "remediation": null },
|
|
7
|
+
{ "diagnostic_id": "d_config_schema_invalid", "code": "CONFIG_SCHEMA_INVALID", "severity": "error", "phase": "config", "scope": "invocation", "subject_id": null, "message": "invalid config", "metadata": { "config_path": "rails_mmd.yml", "field_path": "$.domains" }, "artifact_refs": [], "remediation": null },
|
|
8
|
+
{ "diagnostic_id": "d_config_domain_not_found", "code": "CONFIG_DOMAIN_NOT_FOUND", "severity": "error", "phase": "config", "scope": "domain", "subject_id": "missing", "message": "domain missing", "metadata": { "domain_id": "missing" }, "artifact_refs": [], "remediation": null },
|
|
9
|
+
{ "diagnostic_id": "d_output_directory_invalid", "code": "OUTPUT_DIRECTORY_INVALID", "severity": "error", "phase": "output_setup", "scope": "invocation", "subject_id": null, "message": "bad output dir", "metadata": { "field_path": "$.output.directory", "reason": "absolute" }, "artifact_refs": [], "remediation": null },
|
|
10
|
+
{ "diagnostic_id": "d_domain_model_not_found", "code": "DOMAIN_MODEL_NOT_FOUND", "severity": "error", "phase": "domain_resolution", "scope": "domain", "subject_id": "core:MissingUser", "message": "model missing", "metadata": { "domain_id": "core", "ruby_constant": "MissingUser" }, "artifact_refs": [], "remediation": null },
|
|
11
|
+
{ "diagnostic_id": "d_domain_model_not_renderable", "code": "DOMAIN_MODEL_NOT_RENDERABLE", "severity": "error", "phase": "domain_resolution", "scope": "model", "subject_id": "core:Admin::User", "message": "model not renderable", "metadata": { "domain_id": "core", "ruby_constant": "Admin::User", "renderability_reason": "abstract" }, "artifact_refs": [], "remediation": null },
|
|
12
|
+
{ "diagnostic_id": "d_domain_empty", "code": "DOMAIN_EMPTY", "severity": "error", "phase": "domain_resolution", "scope": "domain", "subject_id": "core", "message": "domain empty", "metadata": { "domain_id": "core" }, "artifact_refs": [], "remediation": null },
|
|
13
|
+
{ "diagnostic_id": "d_rails_load_failed", "code": "RAILS_LOAD_FAILED", "severity": "error", "phase": "rails_load", "scope": "invocation", "subject_id": null, "message": "rails load failed", "metadata": { "exception_class": "RuntimeError", "exception_summary": "load failed", "backtrace": null }, "artifact_refs": [], "remediation": null },
|
|
14
|
+
{ "diagnostic_id": "d_rails_eager_load_failed", "code": "RAILS_EAGER_LOAD_FAILED", "severity": "error", "phase": "rails_load", "scope": "invocation", "subject_id": null, "message": "eager load failed", "metadata": { "exception_class": "RuntimeError", "exception_summary": "eager failed", "backtrace": null }, "artifact_refs": [], "remediation": null },
|
|
15
|
+
{ "diagnostic_id": "d_multi_db_unsupported", "code": "MULTI_DB_UNSUPPORTED", "severity": "error", "phase": "schema_probe", "scope": "domain", "subject_id": "core", "message": "multi database unsupported", "metadata": { "domain_id": "core", "connection_context_ids": ["{\"name\":\"primary\",\"role\":\"writing\",\"shard\":\"default\"}", "{\"name\":\"animals\",\"role\":\"writing\",\"shard\":\"default\"}"] }, "artifact_refs": [], "remediation": null },
|
|
16
|
+
{ "diagnostic_id": "d_model_table_missing", "code": "MODEL_TABLE_MISSING", "severity": "error", "phase": "schema_probe", "scope": "model", "subject_id": "core:User", "message": "table missing", "metadata": { "domain_id": "core", "ruby_constant": "User", "table_name": "users" }, "artifact_refs": [], "remediation": null },
|
|
17
|
+
{ "diagnostic_id": "d_model_primary_key_unsupported", "code": "MODEL_PRIMARY_KEY_UNSUPPORTED", "severity": "error", "phase": "schema_probe", "scope": "model", "subject_id": "core:User", "message": "pk unsupported", "metadata": { "domain_id": "core", "ruby_constant": "User", "table_name": "users" }, "artifact_refs": [], "remediation": null },
|
|
18
|
+
{ "diagnostic_id": "d_association_target_unresolved", "code": "ASSOCIATION_TARGET_UNRESOLVED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "users.account", "message": "target unresolved", "metadata": { "domain_id": "core", "owner_constant": "User", "association_name": "account", "target_constant": "Account" }, "artifact_refs": [], "remediation": null },
|
|
19
|
+
{ "diagnostic_id": "d_association_through_unresolved", "code": "ASSOCIATION_THROUGH_UNRESOLVED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "authors.teams", "message": "through reflection unresolved", "metadata": { "domain_id": "core", "owner_constant": "Author", "association_name": "teams" }, "artifact_refs": [], "remediation": null },
|
|
20
|
+
{ "diagnostic_id": "d_association_source_unresolved", "code": "ASSOCIATION_SOURCE_UNRESOLVED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "authors.teams", "message": "source reflection unresolved", "metadata": { "domain_id": "core", "owner_constant": "Author", "association_name": "teams" }, "artifact_refs": [], "remediation": null },
|
|
21
|
+
{ "diagnostic_id": "d_association_target_not_renderable_omitted", "code": "ASSOCIATION_TARGET_NOT_RENDERABLE_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "users.account", "message": "target omitted", "metadata": { "domain_id": "core", "owner_constant": "User", "association_name": "account", "target_constant": "Account" }, "artifact_refs": [], "remediation": null },
|
|
22
|
+
{ "diagnostic_id": "d_association_polymorphic_omitted", "code": "ASSOCIATION_POLYMORPHIC_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "comments.commentable", "message": "polymorphic omitted", "metadata": { "domain_id": "core", "owner_constant": "Comment", "association_name": "commentable" }, "artifact_refs": [], "remediation": null },
|
|
23
|
+
{ "diagnostic_id": "d_association_polymorphic_targets_unresolved", "code": "ASSOCIATION_POLYMORPHIC_TARGETS_UNRESOLVED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "comments.commentable", "message": "polymorphic targets unresolved", "metadata": { "domain_id": "core", "owner_constant": "Comment", "association_name": "commentable" }, "artifact_refs": [], "remediation": null },
|
|
24
|
+
{ "diagnostic_id": "d_association_join_table_invalid", "code": "ASSOCIATION_JOIN_TABLE_INVALID", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "authors.tags", "message": "join table invalid", "metadata": { "domain_id": "core", "owner_constant": "Author", "association_name": "tags", "target_constant": "Tag", "join_table": "authors_tags", "reason": "unresolved" }, "artifact_refs": [], "remediation": null },
|
|
25
|
+
{ "diagnostic_id": "d_association_scoped_omitted", "code": "ASSOCIATION_SCOPED_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "orders.account", "message": "scoped omitted", "metadata": { "domain_id": "core", "owner_constant": "Order", "association_name": "account" }, "artifact_refs": [], "remediation": null },
|
|
26
|
+
{ "diagnostic_id": "d_association_composite_key_omitted", "code": "ASSOCIATION_COMPOSITE_KEY_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "orders.account", "message": "composite key omitted", "metadata": { "domain_id": "core", "owner_constant": "Order", "association_name": "account" }, "artifact_refs": [], "remediation": null },
|
|
27
|
+
{ "diagnostic_id": "d_association_key_column_missing", "code": "ASSOCIATION_KEY_COLUMN_MISSING", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "orders.account", "message": "key column missing", "metadata": { "domain_id": "core", "owner_constant": "Order", "association_name": "account" }, "artifact_refs": [], "remediation": null },
|
|
28
|
+
{ "diagnostic_id": "d_association_non_primary_key_omitted", "code": "ASSOCIATION_NON_PRIMARY_KEY_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "orders.account", "message": "non primary key omitted", "metadata": { "domain_id": "core", "owner_constant": "Order", "association_name": "account" }, "artifact_refs": [], "remediation": null },
|
|
29
|
+
{ "diagnostic_id": "d_association_name_unsupported_omitted", "code": "ASSOCIATION_NAME_UNSUPPORTED_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "orders.Account", "message": "association name unsupported", "metadata": { "domain_id": "core", "owner_constant": "Order", "association_name": "Account" }, "artifact_refs": [], "remediation": null },
|
|
30
|
+
{ "diagnostic_id": "d_association_macro_omitted", "code": "ASSOCIATION_MACRO_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "authors.posts", "message": "association macro omitted", "metadata": { "domain_id": "core", "owner_constant": "Author", "association_name": "posts", "association_macro": "has_many" }, "artifact_refs": [], "remediation": null },
|
|
31
|
+
{ "diagnostic_id": "d_domain_relationship_omitted", "code": "DOMAIN_RELATIONSHIP_OMITTED", "severity": "warning", "phase": "relationship_build", "scope": "relationship", "subject_id": "orders.account", "message": "outside domain omitted", "metadata": { "domain_id": "core", "owner_constant": "Order", "association_name": "account", "target_constant": "Account" }, "artifact_refs": [], "remediation": null },
|
|
32
|
+
{ "diagnostic_id": "d_db_metadata_degraded", "code": "DB_METADATA_DEGRADED", "severity": "warning", "phase": "schema_probe", "scope": "domain", "subject_id": "core", "message": "metadata degraded", "metadata": { "domain_id": "core", "ruby_constant": "User", "metadata_kind": "foreign_key", "reason": "permission" }, "artifact_refs": [], "remediation": null },
|
|
33
|
+
{ "diagnostic_id": "d_safe_token_collision", "code": "SAFE_TOKEN_COLLISION", "severity": "warning", "phase": "tokenization", "scope": "artifact", "subject_id": "core:er", "message": "token collision resolved", "metadata": { "artifact_kind": "er", "domain_id": "core", "token_kind": "entity", "base_safe_token": "USER", "collision_subject_count": 2, "resolved": true }, "artifact_refs": [{ "artifact_kind": "render_plan", "domain_id": "core", "path": "core.er.render_plan.json" }], "remediation": null },
|
|
34
|
+
{ "diagnostic_id": "d_mermaid_serialization_failed", "code": "MERMAID_SERIALIZATION_FAILED", "severity": "error", "phase": "serialization", "scope": "artifact", "subject_id": "core:er", "message": "serialization failed", "metadata": { "artifact_kind": "er", "domain_id": "core", "reason": "invalid token" }, "artifact_refs": [], "remediation": null },
|
|
35
|
+
{ "diagnostic_id": "d_output_write_failed", "code": "OUTPUT_WRITE_FAILED", "severity": "error", "phase": "publish", "scope": "invocation", "subject_id": null, "message": "write failed", "metadata": { "operation": "rename", "path": "core.er.mmd" }, "artifact_refs": [], "remediation": null },
|
|
36
|
+
{ "diagnostic_id": "d_internal_error", "code": "INTERNAL_ERROR", "severity": "error", "phase": "internal", "scope": "invocation", "subject_id": null, "message": "internal error", "metadata": { "exception_class": "RuntimeError", "exception_summary": "unexpected", "backtrace": null }, "artifact_refs": [], "remediation": null }
|
|
37
|
+
],
|
|
38
|
+
"digest_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_mmd/redactor'
|
|
4
|
+
|
|
5
|
+
module RailsMmd
|
|
6
|
+
# Sanitizes and validates diagnostic artifact references.
|
|
7
|
+
class ArtifactRefSanitizer
|
|
8
|
+
ALLOWED_KEYS = %w[artifact_kind domain_id path].freeze
|
|
9
|
+
ARTIFACT_KINDS = %w[diagnostics er class render_plan stderr].freeze
|
|
10
|
+
DOMAIN_ID_PATTERN = /\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/
|
|
11
|
+
PATH_PATTERN = %r{\A(?!/)(?!.*(?:^|/)\.\.(?:/|$))(?!.*//)[A-Za-z0-9._/-]+\z}
|
|
12
|
+
REQUIRED_KEYS = %w[artifact_kind domain_id].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(redactor: Redactor.new)
|
|
15
|
+
@redactor = redactor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sanitize(artifact_refs)
|
|
19
|
+
artifact_refs.map do |artifact_ref|
|
|
20
|
+
sanitize_reference(artifact_ref)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :redactor
|
|
27
|
+
|
|
28
|
+
def sanitize_reference(artifact_ref)
|
|
29
|
+
reference = artifact_ref.to_h.transform_keys(&:to_s)
|
|
30
|
+
unknown_keys = reference.keys - ALLOWED_KEYS
|
|
31
|
+
raise ArgumentError, "unknown artifact ref keys: #{unknown_keys.join(', ')}" unless unknown_keys.empty?
|
|
32
|
+
|
|
33
|
+
validate_reference!(reference)
|
|
34
|
+
reference['path'] = safe_path(reference.fetch('path')) if reference.key?('path')
|
|
35
|
+
reference
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_reference!(reference)
|
|
39
|
+
missing_keys = REQUIRED_KEYS - reference.keys
|
|
40
|
+
raise ArgumentError, "missing artifact ref keys: #{missing_keys.join(', ')}" unless missing_keys.empty?
|
|
41
|
+
unless ARTIFACT_KINDS.include?(reference.fetch('artifact_kind'))
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"invalid artifact_kind: #{reference.fetch('artifact_kind')}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
domain_id = reference.fetch('domain_id')
|
|
47
|
+
return if domain_id.nil? || domain_id.match?(DOMAIN_ID_PATTERN)
|
|
48
|
+
|
|
49
|
+
raise ArgumentError, "invalid artifact domain_id: #{domain_id}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def safe_path(path)
|
|
53
|
+
path = redactor.sanitize(path)
|
|
54
|
+
return path if path.match?(PATH_PATTERN)
|
|
55
|
+
|
|
56
|
+
raise ArgumentError, "invalid artifact path: #{path}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsMmd
|
|
4
|
+
# Normalizes DB/Rails attribute type hints for Mermaid-facing render plans.
|
|
5
|
+
module AttributeTypes
|
|
6
|
+
SUPPORTED_TYPES = %w[
|
|
7
|
+
bigint
|
|
8
|
+
boolean
|
|
9
|
+
date
|
|
10
|
+
datetime
|
|
11
|
+
decimal
|
|
12
|
+
float
|
|
13
|
+
integer
|
|
14
|
+
string
|
|
15
|
+
text
|
|
16
|
+
time
|
|
17
|
+
unknown
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def normalize(value)
|
|
23
|
+
type = value.to_s.downcase.gsub(/[^a-z0-9]+/, '_').gsub(/\A_+|_+\z/, '')
|
|
24
|
+
return type if SUPPORTED_TYPES.include?(type)
|
|
25
|
+
|
|
26
|
+
'unknown'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json/canonicalization'
|
|
5
|
+
|
|
6
|
+
module RailsMmd
|
|
7
|
+
# RFC 8785/JCS canonical JSON helpers.
|
|
8
|
+
module CanonicalJson
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def dump(value)
|
|
12
|
+
value.to_json_c14n
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def digest_sha256(value)
|
|
16
|
+
Digest::SHA256.hexdigest(dump(value))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'rails_mmd/config'
|
|
5
|
+
require 'rails_mmd/generate_command'
|
|
6
|
+
require_relative 'version'
|
|
7
|
+
|
|
8
|
+
module RailsMmd
|
|
9
|
+
# Minimal Thor shell for smokeable help and version output.
|
|
10
|
+
class CLI < Thor
|
|
11
|
+
MISSING_OPTION_VALUE = Object.new.freeze
|
|
12
|
+
GENERATE_HELP = <<~HELP
|
|
13
|
+
Usage:
|
|
14
|
+
rails-mmd generate
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
[--config=CONFIG] Required YAML file (default: rails_mmd.yml)
|
|
18
|
+
[--output-dir=OUTPUT_DIR] Project-relative directory (default: docs/rails_mmd)
|
|
19
|
+
[--domain=DOMAIN] One configured domain ID (default: all domains)
|
|
20
|
+
[--format=FORMAT] er, class, or both (default: both)
|
|
21
|
+
[--fail-on-warning] Exit 1 if any warning is emitted (default: false)
|
|
22
|
+
|
|
23
|
+
Run from the target Rails application root with bundle exec rails-mmd generate.
|
|
24
|
+
Config output.directory and output.format override the defaults; CLI options take precedence.
|
|
25
|
+
HELP
|
|
26
|
+
|
|
27
|
+
package_name 'rails-mmd'
|
|
28
|
+
remove_command :tree
|
|
29
|
+
default_task :help
|
|
30
|
+
check_unknown_options! only: :generate
|
|
31
|
+
|
|
32
|
+
def self.exit_on_failure?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc 'version', 'Print rails-mmd version'
|
|
37
|
+
map '--version' => :version, '-v' => :version
|
|
38
|
+
def version
|
|
39
|
+
say RailsMmd::VERSION
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc 'generate', 'Generate Mermaid diagrams from a Rails app'
|
|
43
|
+
method_option :config, type: :string, lazy_default: MISSING_OPTION_VALUE
|
|
44
|
+
method_option :output_dir, type: :string, lazy_default: MISSING_OPTION_VALUE
|
|
45
|
+
method_option :domain, type: :string, lazy_default: MISSING_OPTION_VALUE
|
|
46
|
+
method_option :format, type: :string, lazy_default: MISSING_OPTION_VALUE
|
|
47
|
+
method_option :fail_on_warning, type: :string
|
|
48
|
+
method_option :help, type: :boolean, hide: true, aliases: '-h'
|
|
49
|
+
def generate(*args)
|
|
50
|
+
return say(GENERATE_HELP) if options[:help]
|
|
51
|
+
raise Thor::Error, 'Unexpected arguments. Run rails-mmd generate --help for usage.' unless args.empty?
|
|
52
|
+
|
|
53
|
+
result = GenerateCommand.new.run(cli_options: cli_options, fail_on_warning: fail_on_warning?)
|
|
54
|
+
emit_result(result)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
no_commands do
|
|
58
|
+
def cli_options
|
|
59
|
+
missing = options.key(MISSING_OPTION_VALUE)
|
|
60
|
+
raise Thor::Error, "No value provided for option '--#{missing.tr('_', '-')}'" if missing
|
|
61
|
+
|
|
62
|
+
Config::CliOptions.new(
|
|
63
|
+
config_path: options[:config],
|
|
64
|
+
output_dir: options[:output_dir],
|
|
65
|
+
domain: options[:domain],
|
|
66
|
+
format: options[:format]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def emit_result(result)
|
|
71
|
+
print result.stdout unless result.stdout.empty?
|
|
72
|
+
warn result.stderr unless result.stderr.empty?
|
|
73
|
+
exit(result.exit_code)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fail_on_warning?
|
|
77
|
+
return false if options[:fail_on_warning].nil?
|
|
78
|
+
return false if options[:fail_on_warning] == 'false'
|
|
79
|
+
return true if options[:fail_on_warning] == true
|
|
80
|
+
return true if %w[true fail_on_warning].include?(options[:fail_on_warning]) || options[:fail_on_warning].empty?
|
|
81
|
+
|
|
82
|
+
raise Thor::Error, '--fail-on-warning must be true or false'
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|