rails-schema 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1275bccb3e5144d351dcf4445209299ee269dfae8e75b4176e8a91bb85b5ccc2
4
+ data.tar.gz: 2ba3e983963eaef3a933ba9d99eebd7b3221acdaca492212ddda04d5a0354122
5
+ SHA512:
6
+ metadata.gz: bade47644111e000883867b876dad54b59aa07df5800719185c48cd9d2307fa030d3d1df38c00a738be94bd2c37426bbe5d181729dc9b0995872103449332e1b
7
+ data.tar.gz: ee1eaa1f3050778be38ae59a69269b93658e149466cc135ec7eb58cbc34cbf2ec814ea99d665777887c64ff341440351a63ac9707a791add32e9fb30d152ab52
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Andrei Kislichenko
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.
data/PROJECT.md ADDED
@@ -0,0 +1,322 @@
1
+ # Rails::Schema — Project Design
2
+
3
+ **A Ruby gem that generates an interactive HTML/JS/CSS page to visualize the database schema of a Rails application.**
4
+
5
+ ---
6
+
7
+ ## 1. Gem Overview
8
+
9
+ **Name:** `rails-schema`
10
+ **Module:** `Rails::Schema`
11
+ **Version:** `0.1.0`
12
+
13
+ Rails::Schema introspects a Rails app's models, associations, and database columns at runtime, then generates a single self-contained HTML file with an interactive, explorable entity-relationship diagram. No external server, no SaaS dependency — just one command and a browser.
14
+
15
+ ```bash
16
+ # Rake task
17
+ rake rails_schema:generate
18
+
19
+ # Programmatic
20
+ Rails::Schema.generate(output: "docs/schema.html")
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 2. Architecture
26
+
27
+ ```
28
+ ┌──────────────────────────────────────────────────────┐
29
+ │ rails-schema gem │
30
+ ├──────────────┬──────────────┬────────────────────────┤
31
+ │ Extractor │ Transformer │ Renderer │
32
+ │ (Ruby) │ (Ruby) │ (ERB → HTML/JS/CSS) │
33
+ ├──────────────┼──────────────┼────────────────────────┤
34
+ │ Reads Rails │ Builds a │ Produces a single │
35
+ │ models, │ normalized │ self-contained .html │
36
+ │ reflections, │ graph JSON │ file with embedded │
37
+ │ schema.rb, │ structure │ JS app + CSS │
38
+ │ columns │ │ │
39
+ └──────────────┴──────────────┴────────────────────────┘
40
+ ```
41
+
42
+ ### 2.1 Layer Breakdown
43
+
44
+ | Layer | Responsibility | Key Classes |
45
+ |---|---|---|
46
+ | **Extractor** | Introspects Rails environment; collects models, columns, associations | `Rails::Schema::Extractor::ModelScanner`, `ColumnReader`, `AssociationReader`, `SchemaFileParser` |
47
+ | **Transformer** | Normalizes extracted data into a serializable graph structure (nodes + edges + metadata) | `Rails::Schema::Transformer::GraphBuilder`, `Node`, `Edge` |
48
+ | **Renderer** | Takes the graph data and injects it into an HTML/JS/CSS template using ERB | `Rails::Schema::Renderer::HtmlGenerator` |
49
+ | **Railtie** | Provides the `rails_schema:generate` rake task | `Rails::Schema::Railtie` |
50
+
51
+ ### 2.2 Generation Pipeline
52
+
53
+ ```ruby
54
+ def generate(output: nil)
55
+ schema_data = Extractor::SchemaFileParser.new.parse
56
+ models = Extractor::ModelScanner.new(schema_data: schema_data).scan
57
+ column_reader = Extractor::ColumnReader.new(schema_data: schema_data)
58
+ graph_data = Transformer::GraphBuilder.new(column_reader: column_reader).build(models)
59
+ generator = Renderer::HtmlGenerator.new(graph_data: graph_data)
60
+ generator.render_to_file(output)
61
+ end
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 3. Data Extraction Strategy
67
+
68
+ ### 3.1 Sources of Truth
69
+
70
+ 1. **`db/schema.rb` parsing** — `SchemaFileParser` parses the schema file line-by-line with regex to extract table names, column definitions (name, type, nullable, default), and primary key info. This is attempted first and used as a fast, database-free source.
71
+ 2. **ActiveRecord reflection API** — `AssociationReader` uses `Model.reflect_on_all_associations` for associations (`has_many`, `belongs_to`, `has_one`, `has_and_belongs_to_many`), including `:through` and `:polymorphic`.
72
+ 3. **`Model.columns`** — `ColumnReader` falls back to `model.columns` via ActiveRecord when a table is not found in schema_data.
73
+
74
+ ### 3.2 Model Discovery
75
+
76
+ `ModelScanner` discovers models by:
77
+
78
+ 1. Calling `Rails.application.eager_load!` (with Zeitwerk support and multiple fallback strategies)
79
+ 2. Collecting `ActiveRecord::Base.descendants`
80
+ 3. Filtering out abstract classes, anonymous classes, and models without known tables
81
+ 4. Applying `exclude_models` configuration (supports wildcard prefix matching like `"ActiveStorage::*"`)
82
+ 5. Returning models sorted by name
83
+
84
+ When `schema_data` is available, table existence is checked against parsed schema data instead of hitting the database.
85
+
86
+ ### 3.3 Schema File Parser
87
+
88
+ `SchemaFileParser` provides database-free column extraction:
89
+
90
+ - Parses `create_table` blocks from `db/schema.rb`
91
+ - Extracts column types, names, nullability, and defaults (string, numeric, boolean)
92
+ - Handles custom primary key types (`id: :uuid`, `id: :bigint`) and `id: false`
93
+ - Skips index definitions
94
+
95
+ ### 3.4 Intermediate Data Format (JSON Graph)
96
+
97
+ ```json
98
+ {
99
+ "nodes": [
100
+ {
101
+ "id": "User",
102
+ "table": "users",
103
+ "columns": [
104
+ { "name": "id", "type": "bigint", "primary": true },
105
+ { "name": "email", "type": "string", "nullable": false },
106
+ { "name": "name", "type": "string", "nullable": true }
107
+ ]
108
+ }
109
+ ],
110
+ "edges": [
111
+ {
112
+ "from": "User",
113
+ "to": "Post",
114
+ "type": "has_many",
115
+ "through": null,
116
+ "foreign_key": "user_id",
117
+ "polymorphic": false,
118
+ "label": "posts"
119
+ }
120
+ ],
121
+ "metadata": {
122
+ "rails_version": "7.2.0",
123
+ "generated_at": "2026-02-15T12:00:00Z",
124
+ "model_count": 42
125
+ }
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 4. Interactive Frontend Design
132
+
133
+ The generated HTML file is a **single self-contained file** — no CDN dependencies, no network requests. All JS and CSS are inlined. The JSON graph is embedded as a `<script>` tag.
134
+
135
+ ### 4.1 Technology Choices
136
+
137
+ | Concern | Choice | Rationale |
138
+ |---|---|---|
139
+ | Graph rendering | **SVG + d3-force** (vendored/minified) | DOM-level interactivity, good for typical schema sizes |
140
+ | Layout algorithm | Force-directed (d3-force) | Natural clustering of related models |
141
+ | UI framework | Vanilla JS | Zero dependencies, small file size |
142
+ | Styling | CSS custom properties + embedded stylesheet | Theming support, dark/light mode |
143
+
144
+ ### 4.2 Implemented Interactive Features
145
+
146
+ #### A. Model Selector Panel (left sidebar, 280px)
147
+
148
+ - Searchable list of all models with filtering
149
+ - Multi-select checkboxes to toggle visibility
150
+ - Model count display
151
+
152
+ #### B. Canvas / Diagram Area (center)
153
+
154
+ - **Nodes** = model cards showing:
155
+ - Model name (bold header)
156
+ - Column list (expandable/collapsible — collapsed by default)
157
+ - Primary key highlighted
158
+ - Column types shown in a muted typeface
159
+ - **Edges** = association lines:
160
+ - Color-coded by association type
161
+ - Labels on hover (association name + foreign key)
162
+ - **Force-directed layout** that stabilizes, then allows manual drag-and-drop
163
+
164
+ #### C. Zoom & Navigation
165
+
166
+ - Scroll-wheel zoom with smooth interpolation
167
+ - Pinch-to-zoom on trackpads
168
+ - Fit-to-screen button
169
+ - Zoom-to-selection (click a model in sidebar to center on it)
170
+
171
+ #### D. Focus Mode
172
+
173
+ When a user clicks on a model node:
174
+
175
+ 1. The selected model and its directly associated models are highlighted
176
+ 2. All other nodes and edges fade to reduced opacity
177
+ 3. A detail panel (right sidebar, 320px) shows full column/association info
178
+ 4. Press `Esc` or click background to exit
179
+
180
+ #### E. Toolbar (48px)
181
+
182
+ - Dark / Light theme toggle (respects `prefers-color-scheme`)
183
+ - Fit-to-screen button
184
+ - Keyboard shortcuts: `/` to focus search, `Esc` to deselect
185
+
186
+ ---
187
+
188
+ ## 5. Configuration
189
+
190
+ ```ruby
191
+ # config/initializers/rails_schema.rb
192
+ Rails::Schema.configure do |config|
193
+ config.output_path = "docs/schema.html" # Output file location
194
+ config.exclude_models = [] # Models to exclude (supports "Namespace::*" wildcards)
195
+ config.title = "Database Schema" # Page title
196
+ config.theme = :auto # :light, :dark, :auto
197
+ config.expand_columns = false # Start with columns expanded
198
+ end
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 6. Gem Structure
204
+
205
+ ```
206
+ rails-schema/
207
+ ├── lib/
208
+ │ ├── rails/schema.rb # Entry point, configuration DSL, generate method
209
+ │ └── rails/schema/
210
+ │ ├── version.rb # VERSION = "0.1.0"
211
+ │ ├── configuration.rb # Config object (5 attributes)
212
+ │ ├── railtie.rb # Rails integration, rake task
213
+ │ ├── extractor/
214
+ │ │ ├── model_scanner.rb # Discovers AR models
215
+ │ │ ├── association_reader.rb # Reads reflections
216
+ │ │ ├── column_reader.rb # Reads columns (schema_data or AR)
217
+ │ │ └── schema_file_parser.rb # Parses db/schema.rb
218
+ │ ├── transformer/
219
+ │ │ ├── graph_builder.rb # Builds node/edge graph
220
+ │ │ ├── node.rb # Value object
221
+ │ │ └── edge.rb # Value object
222
+ │ ├── renderer/
223
+ │ │ └── html_generator.rb # ERB rendering, asset inlining
224
+ │ └── assets/
225
+ │ ├── template.html.erb # Main HTML template
226
+ │ ├── app.js # Interactive frontend (vanilla JS)
227
+ │ ├── style.css # Stylesheet with CSS custom properties
228
+ │ └── vendor/
229
+ │ └── d3.min.js # Vendored d3 library
230
+ ├── spec/
231
+ │ ├── spec_helper.rb
232
+ │ ├── support/
233
+ │ │ └── test_models.rb # User, Post, Comment, Tag models
234
+ │ └── rails/schema/
235
+ │ ├── rails_schema_spec.rb
236
+ │ ├── configuration_spec.rb
237
+ │ ├── extractor/
238
+ │ │ ├── model_scanner_spec.rb
239
+ │ │ ├── column_reader_spec.rb
240
+ │ │ ├── association_reader_spec.rb
241
+ │ │ └── schema_file_parser_spec.rb
242
+ │ ├── transformer/
243
+ │ │ └── graph_builder_spec.rb
244
+ │ └── renderer/
245
+ │ └── html_generator_spec.rb
246
+ ├── Gemfile
247
+ ├── rails-schema.gemspec
248
+ ├── LICENSE.txt
249
+ └── README.md
250
+ ```
251
+
252
+ ---
253
+
254
+ ## 7. Key Design Decisions
255
+
256
+ ### Why a single HTML file?
257
+
258
+ - **Zero deployment friction** — open in any browser, share via Slack/email, commit to repo
259
+ - **Offline-first** — works on airplane mode, no CDN failures
260
+ - **Portable** — CI can generate it, GitHub Pages can host it, anyone can view it
261
+
262
+ ### Why not a mounted Rails engine?
263
+
264
+ A mounted engine requires a running server. A static file can be generated in CI, committed to the repo, and opened by anyone — including non-developers looking at a data model.
265
+
266
+ ### Why parse schema.rb?
267
+
268
+ Parsing `db/schema.rb` allows column extraction without a database connection. This means the gem can work in CI environments or development setups where the database isn't running. It also avoids eager-loading the entire app just to read column metadata.
269
+
270
+ ### Why force-directed layout?
271
+
272
+ It handles unknown schemas gracefully — you don't need to pre-define positions. Combined with drag-and-drop repositioning, it gives the best default experience.
273
+
274
+ ---
275
+
276
+ ## 8. Dependencies
277
+
278
+ ```ruby
279
+ # rails-schema.gemspec
280
+ spec.add_dependency "activerecord", ">= 6.0"
281
+ spec.add_dependency "railties", ">= 6.0"
282
+
283
+ # Development
284
+ # rspec (~> 3.0), rubocop (~> 1.21), sqlite3
285
+ ```
286
+
287
+ **Zero runtime JS dependencies shipped to the user** — d3 is vendored and minified into the template. The HTML file has no external requests.
288
+
289
+ ---
290
+
291
+ ## 9. Testing Strategy
292
+
293
+ | Layer | Approach |
294
+ |---|---|
295
+ | Extractor | Unit tests with in-memory SQLite models (User, Post, Comment, Tag) |
296
+ | Transformer | Pure Ruby unit tests — graph building, edge filtering |
297
+ | Renderer | Output tests — verify HTML structure, embedded data, script injection safety |
298
+ | Configuration | Unit tests for defaults and attribute setting |
299
+
300
+ **66 tests, all passing.** Run with `bundle exec rspec`.
301
+
302
+ ---
303
+
304
+ ## 10. Future Enhancements (Roadmap)
305
+
306
+ 1. **CLI executable** — `bundle exec rails_schema` binary for standalone usage
307
+ 2. **Live mode** — a mounted Rails engine with hot-reload when migrations run
308
+ 3. **Additional layout modes** — hierarchical, circular, grid
309
+ 4. **Validation extraction** — read `Model.validators` for presence, uniqueness constraints
310
+ 5. **STI handling** — group models sharing a table, show children as badges
311
+ 6. **Concern extraction** — display included modules on model nodes
312
+ 7. **Export options** — PNG, SVG, Mermaid ER diagram, raw JSON
313
+ 8. **Schema diff** — compare two generated JSONs and highlight changes
314
+ 9. **Multi-database support** — Rails 6+ multi-DB configs
315
+ 10. **Minimap** — thumbnail overview for large schemas
316
+ 11. **Permalink / State URL** — encode view state in URL hash for sharing
317
+ 12. **Advanced filtering** — `include_only`, namespace grouping, tag-based filters
318
+ 13. **Custom CSS/JS injection** — user-provided assets inlined into output
319
+
320
+ ---
321
+
322
+ *Document reflects the current implementation (v0.1.0). Future enhancements are aspirational and subject to refinement.*
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Rails::Schema
2
+
3
+ Interactive HTML visualization of your Rails database schema. Introspects your app's models, associations, and columns, then generates a single self-contained HTML file with an interactive entity-relationship diagram.
4
+
5
+ No external server, no CDN — just one command and a browser.
6
+
7
+ **[Live example](https://andrew2net.github.io/rails-schema/)** — generated from [Fizzy](https://www.fizzy.do), a modern spin on kanban for tracking just about anything, created by [37signals](https://37signals.com).
8
+
9
+ ![Rails Schema screenshot](docs/screenshot.png)
10
+
11
+ ## Installation
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "rails-schema", group: :development
17
+ ```
18
+
19
+ Then run:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Rake task
28
+
29
+ ```bash
30
+ rake rails_schema:generate
31
+ ```
32
+
33
+ This generates `docs/schema.html` by default. Open it in your browser.
34
+
35
+ ### Programmatic
36
+
37
+ ```ruby
38
+ Rails::Schema.generate(output: "docs/schema.html")
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Create an initializer at `config/initializers/rails_schema.rb`:
44
+
45
+ ```ruby
46
+ Rails::Schema.configure do |config|
47
+ config.output_path = "docs/schema.html"
48
+ config.title = "My App Schema"
49
+ config.theme = :auto # :auto, :light, or :dark
50
+ config.expand_columns = false # start with columns collapsed
51
+ config.exclude_models = [
52
+ "ActiveStorage::Blob",
53
+ "ActiveStorage::Attachment",
54
+ "ActionMailbox::*" # wildcard prefix matching
55
+ ]
56
+ end
57
+ ```
58
+
59
+ ## How it works
60
+
61
+ The gem parses your `db/schema.rb` file to extract table and column information — **no database connection required**. It also introspects loaded ActiveRecord models for association metadata. This means the gem works even if you don't have a local database set up, as long as `db/schema.rb` is present (which is standard in Rails projects under version control).
62
+
63
+ ## Features
64
+
65
+ - **No database required** — reads directly from `db/schema.rb`
66
+ - **Force-directed layout** — models cluster naturally by association density
67
+ - **Searchable sidebar** — filter models by name or table
68
+ - **Click-to-focus** — click a model to highlight its neighborhood, fading unrelated models
69
+ - **Detail panel** — full column list and associations for the selected model
70
+ - **Dark/light theme** — toggle or auto-detect from system preference
71
+ - **Zoom & pan** — scroll wheel, pinch, or buttons
72
+ - **Keyboard shortcuts** — `/` search, `Esc` deselect, `+/-` zoom, `F` fit to screen
73
+ - **Self-contained** — single HTML file with all CSS, JS, and data inlined
74
+
75
+ ## License
76
+
77
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]