canvas_erd 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: 5b593a4a8e3ebb6c820c3469cc0063acb4fdf129313e2ea2c68da33a15166be2
4
+ data.tar.gz: da8d50366c574f574289cf59046e98954fdf324ceff1f20d3ba2811349a5b35e
5
+ SHA512:
6
+ metadata.gz: 8262355aa351a042ea353f9d1340aee15d30e839f29bcdea12c3d3d728070d176deb0cb845db2b43e7c1f0cb05cd8321a8480e00286e665379b1b368640c0f31
7
+ data.tar.gz: a4db10469eaf6ee4f1d95afbcc7f02f826c37f0234187d02d925c79d73f4c65f86c9fe5dbb8e08d6e46929189c6e53d5695f81f05e185d2a01c73349e88b1776
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-08-30
4
+
5
+ - Generate a JSON-safe diagram schema from Rails ERD domain models.
6
+ - Start a loopback-only editor using the Rails application's Rack server.
7
+ - Arrange and select tables on a Fabric.js canvas, with editable notes, arrows, zoom, and pan controls.
8
+ - Refresh columns and relationships while preserving diagram edits.
9
+ - Save editable diagrams inside ordinary PNG files and reopen them from the CLI.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marc Heiligers, FASCINATION•works
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,86 @@
1
+ # CanvasERD
2
+
3
+ CanvasERD is a companion gem for editing diagrams generated from Rails ERD domain models in a local browser.
4
+
5
+ The editor renders Rails models and relationships on an interactive Fabric.js canvas.
6
+ New diagrams use ELK's layered graph layout to arrange tables from their Rails relationships.
7
+
8
+ ## Installation
9
+
10
+ Add CanvasERD to the development group in the Rails application's `Gemfile`:
11
+
12
+ ```ruby
13
+ group :development do
14
+ gem "canvas_erd", require: false
15
+ end
16
+ ```
17
+
18
+ Then run `bundle install`. CanvasERD requires Ruby 3.1 or newer, Active Record 7.0 or newer, and a Rack server available in the Rails bundle.
19
+
20
+ ## Usage
21
+
22
+ From a Rails application root, run:
23
+
24
+ ```sh
25
+ bundle exec canvas_erd
26
+ ```
27
+
28
+ CanvasERD loads and eager-loads the Rails application, generates the schema, starts on a loopback-only address, and opens the editor in the default browser. Pass `--no-open` to print the URL without opening it.
29
+
30
+ CanvasERD uses Rackup with a server handler already present in the Rails bundle, normally Puma. It does not install a separate web server.
31
+
32
+ Diagrams are saved by the editor as `*.erd.png` files under `docs/erd/` in the Rails application. Use a different directory inside the application with `--diagrams-dir`, for example:
33
+
34
+ ```sh
35
+ bundle exec canvas_erd --diagrams-dir db/diagrams
36
+ ```
37
+
38
+ The editor's saved-diagram menu loads these files directly. You can also open a diagram when starting CanvasERD with `bundle exec canvas_erd path/to/domain.erd.png`.
39
+
40
+ ## Editor controls
41
+
42
+ - Drag tables to reposition them.
43
+ - Use the table sidebar to add or remove tables without losing their positions.
44
+ - Use **Layout tables** to rearrange the tables currently on the canvas with ELK without moving notes or hidden tables.
45
+ - Pinch over the canvas or use the toolbar buttons to zoom.
46
+ - Use a two-finger trackpad gesture to pan. Space-drag remains available as a keyboard fallback, and **Fit diagram** resets the view.
47
+ - Add editable notes with **Add note**. Select a note and press Delete to remove it.
48
+ - Use **Refresh schema** after a database migration to update columns and relationships. Existing table positions, selections, notes, and the current view are preserved; new tables start unselected.
49
+ - Edit the diagram name, then use **Save** (or <kbd>⌘S</kbd>/<kbd>Ctrl-S</kbd>) to write the complete diagram into the Rails application as a normal `*.erd.png` image with its editable schema, layout, notes, selection, and viewport embedded inside it. Select a saved diagram from the menu to reopen it, or use **New** to start an “Untitled ERD.”
50
+
51
+ CanvasERD refreshes the model classes already loaded by the server. Restart CanvasERD after adding or renaming model classes or changing model code.
52
+ Image optimization tools may remove the CanvasERD metadata required to edit a diagram again.
53
+
54
+ The editor binds only to `127.0.0.1` and does not expose the Rails application over the network.
55
+
56
+ ## Domain adapter
57
+
58
+ After loading the Rails application, generate a JSON-safe schema hash with:
59
+
60
+ ```ruby
61
+ schema = CanvasERD::Diagram.create
62
+ ```
63
+
64
+ The schema contains entities, attributes, relationships, and specializations with stable identifiers. Relationship cardinality ranges use `nil` as an unbounded maximum.
65
+
66
+ ## Development
67
+
68
+ Run the tests:
69
+
70
+ ```sh
71
+ bundle exec rake test
72
+ ```
73
+
74
+ Build the gem:
75
+
76
+ ```sh
77
+ bundle exec rake build
78
+ ```
79
+
80
+ ## License
81
+
82
+ CanvasERD is available under the MIT License. Dependencies remain subject to their included licenses.
83
+
84
+ [Fabric.js](https://fabricjs.com/) 7.4.0 is vendored under `lib/canvas_erd/web/vendor`. Its license is included in `licenses/FABRIC-JS-LICENSE.txt`.
85
+ [elkjs](https://github.com/kieler/elkjs) 0.12.0 is vendored there as a browser API and Web Worker. Its [source is available from the elkjs project](https://github.com/kieler/elkjs/tree/0.12.0), and its license is included in `licenses/ELKJS-LICENSE.md`.
86
+ Icons from [iconoir](https://iconoir.com/) directly embedded in the HTML, and it's license is included in `licenses/ICONOIR-LICENSE`.
data/exe/canvas_erd ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+
6
+ require "canvas_erd"
7
+
8
+ exit CanvasERD::CLI.start(ARGV)
9
+
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanvasERD
4
+ class ApplicationLoader
5
+ class Error < StandardError; end
6
+
7
+ attr_reader :root
8
+
9
+ def initialize(root = Dir.pwd)
10
+ @root = File.expand_path(root)
11
+ end
12
+
13
+ def load
14
+ environment_path = File.join(root, "config", "environment.rb")
15
+ unless File.file?(environment_path)
16
+ raise Error, "Rails application environment not found at #{environment_path}"
17
+ end
18
+
19
+ require environment_path
20
+ eager_load_rails_application
21
+ true
22
+ end
23
+
24
+ private
25
+
26
+ def eager_load_rails_application
27
+ return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
28
+
29
+ Rails.application.eager_load!
30
+
31
+ config = Rails.application.config
32
+ return unless config.respond_to?(:eager_load_namespaces)
33
+
34
+ config.eager_load_namespaces.each(&:eager_load!)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module CanvasERD
6
+ class Browser
7
+ def self.open(url)
8
+ new.open(url)
9
+ end
10
+
11
+ def initialize(host_os: RbConfig::CONFIG.fetch("host_os"), process_spawn: Process.method(:spawn))
12
+ @host_os = host_os
13
+ @process_spawn = process_spawn
14
+ end
15
+
16
+ def open(url)
17
+ command = command_for(url)
18
+ return false unless command
19
+
20
+ process_id = @process_spawn.call(*command, out: File::NULL, err: File::NULL)
21
+ Process.detach(process_id) if process_id.is_a?(Integer)
22
+ true
23
+ rescue SystemCallError
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ def command_for(url)
30
+ case @host_os
31
+ when /darwin/
32
+ ["open", url]
33
+ when /linux|bsd/
34
+ ["xdg-open", url]
35
+ when /mswin|mingw|cygwin/
36
+ ["cmd", "/c", "start", "", url]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module CanvasERD
6
+ class CLI
7
+ class Error < StandardError; end
8
+
9
+ Options = Struct.new(:action, :diagram_path, :diagrams_directory, :open_browser, keyword_init: true)
10
+
11
+ def self.start(arguments = ARGV, out: $stdout, err: $stderr)
12
+ new(out: out, err: err).start(arguments)
13
+ end
14
+
15
+ def initialize(out: $stdout, err: $stderr, root: Dir.pwd)
16
+ @out = out
17
+ @err = err
18
+ @root = root
19
+ end
20
+
21
+ def start(arguments)
22
+ options = parse(arguments)
23
+
24
+ case options.action
25
+ when :help
26
+ @out.puts parser
27
+ 0
28
+ when :version
29
+ @out.puts "CanvasERD #{CanvasERD::VERSION}"
30
+ 0
31
+ else
32
+ run(options)
33
+ end
34
+ rescue OptionParser::ParseError => error
35
+ @err.puts "Error: #{error.message}"
36
+ @err.puts parser
37
+ 1
38
+ rescue Error, ApplicationLoader::Error, DiagramStore::Error, PngDocument::Error, Server::Error => error
39
+ @err.puts "Error: #{error.message}"
40
+ 1
41
+ rescue StandardError => error
42
+ @err.puts "Failed: #{error.class}: #{error.message}"
43
+ 1
44
+ end
45
+
46
+ def parse(arguments)
47
+ action = :run
48
+ diagrams_directory = DiagramStore::DEFAULT_DIRECTORY
49
+ open_browser = true
50
+ remaining = parser(
51
+ action_setter: ->(value) { action = value },
52
+ directory_setter: ->(value) { diagrams_directory = value },
53
+ open_setter: ->(value) { open_browser = value }
54
+ ).parse(arguments.dup)
55
+
56
+ raise OptionParser::ParseError, "expected at most one diagram path" if remaining.length > 1
57
+
58
+ Options.new(
59
+ action: action,
60
+ diagram_path: remaining.first,
61
+ diagrams_directory: diagrams_directory,
62
+ open_browser: open_browser
63
+ )
64
+ end
65
+
66
+ private
67
+
68
+ def run(options)
69
+ if options.diagram_path
70
+ @err.puts "Loading CanvasERD diagram from #{options.diagram_path}..."
71
+ document = PngDocument.load(File.expand_path(options.diagram_path, @root))
72
+ end
73
+
74
+ @err.puts "Loading Rails application from #{@root}..."
75
+ ApplicationLoader.new(@root).load
76
+
77
+ schema_provider = SchemaProvider.new
78
+ if document
79
+ schema = document.fetch("schema")
80
+ state = document.fetch("state")
81
+ else
82
+ @err.puts "Generating Rails ERD schema..."
83
+ schema = schema_provider.call
84
+ end
85
+ diagram_store = DiagramStore.new(root: @root, directory: options.diagrams_directory)
86
+ app = WebApplication.new(
87
+ schema: schema,
88
+ schema_provider: schema_provider,
89
+ state: state,
90
+ diagram_store: diagram_store
91
+ )
92
+ server = Server.new(app: app)
93
+
94
+ @out.puts "CanvasERD is running at #{server.url}"
95
+ unless !options.open_browser || Browser.open(server.url)
96
+ @err.puts "Could not open a browser. Visit #{server.url} manually."
97
+ end
98
+
99
+ server.start
100
+ 0
101
+ end
102
+
103
+ def parser(action_setter: ->(_value) {}, directory_setter: ->(_value) {}, open_setter: ->(_value) {})
104
+ OptionParser.new do |options|
105
+ options.banner = "Usage: canvas_erd [options] [diagram.png]"
106
+
107
+ options.on("--no-open", "Do not open the editor in a browser") do
108
+ open_setter.call(false)
109
+ end
110
+
111
+ options.on("--diagrams-dir DIRECTORY", "Store diagrams here (default: docs/erd)") do |directory|
112
+ directory_setter.call(directory)
113
+ end
114
+
115
+ options.on("-v", "--version", "Print the CanvasERD version") do
116
+ action_setter.call(:version)
117
+ end
118
+
119
+ options.on("-h", "--help", "Show this help") do
120
+ action_setter.call(:help)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_erd/diagram"
4
+
5
+ module CanvasERD
6
+ class Diagram < RailsERD::Diagram
7
+ setup do
8
+ @entity_ids = {}
9
+ @schema = {
10
+ "name" => domain.name,
11
+ "entities" => [],
12
+ "relationships" => [],
13
+ "specializations" => []
14
+ }
15
+ end
16
+
17
+ each_entity do |entity, attributes|
18
+ @entity_ids[entity.name.to_s] = true
19
+ @schema["entities"] << entity_data(entity, attributes)
20
+ end
21
+
22
+ each_specialization do |specialization|
23
+ next unless included_entity?(specialization.generalized) && included_entity?(specialization.specialized)
24
+
25
+ @schema["specializations"] << specialization_data(specialization)
26
+ end
27
+
28
+ each_relationship do |relationship|
29
+ next unless included_entity?(relationship.source) && included_entity?(relationship.destination)
30
+
31
+ @schema["relationships"] << relationship_data(relationship)
32
+ end
33
+
34
+ save do
35
+ @schema["entities"].sort_by! { |entity| entity.fetch("id") }
36
+ @schema["relationships"].sort_by! { |relationship| relationship.fetch("id") }
37
+ @schema["specializations"].sort_by! { |specialization| specialization.fetch("id") }
38
+ @schema
39
+ end
40
+
41
+ private
42
+
43
+ def entity_data(entity, attributes)
44
+ {
45
+ "id" => entity.name.to_s,
46
+ "name" => entity.name.to_s,
47
+ "label" => entity_label(entity),
48
+ "table_name" => entity.model&.table_name,
49
+ "namespace" => entity.namespace,
50
+ "virtual" => entity.virtual?,
51
+ "generalized" => entity.generalized?,
52
+ "specialized" => entity.specialized?,
53
+ "attributes" => attributes.map { |attribute| attribute_data(entity, attribute) }
54
+ }
55
+ end
56
+
57
+ def entity_label(entity)
58
+ if options[:table_names] && entity.model
59
+ entity.model.table_name
60
+ else
61
+ entity.name.to_s
62
+ end
63
+ end
64
+
65
+ def attribute_data(entity, attribute)
66
+ {
67
+ "id" => "#{entity.name}.#{attribute.name}",
68
+ "name" => attribute.name.to_s,
69
+ "type" => attribute.type.to_s,
70
+ "type_description" => attribute.type_description,
71
+ "primary_key" => attribute.primary_key?,
72
+ "foreign_key" => attribute.foreign_key?,
73
+ "mandatory" => attribute.mandatory?,
74
+ "unique" => attribute.unique?,
75
+ "timestamp" => attribute.timestamp?,
76
+ "inheritance" => attribute.inheritance?
77
+ }
78
+ end
79
+
80
+ def relationship_data(relationship)
81
+ source_id = relationship.source.name.to_s
82
+ destination_id = relationship.destination.name.to_s
83
+
84
+ {
85
+ "id" => endpoint_id("relationship", source_id, destination_id),
86
+ "source_id" => source_id,
87
+ "destination_id" => destination_id,
88
+ "cardinality" => cardinality_data(relationship.cardinality),
89
+ "indirect" => relationship.indirect?,
90
+ "mutual" => relationship.mutual?,
91
+ "recursive" => relationship.recursive?
92
+ }
93
+ end
94
+
95
+ def cardinality_data(cardinality)
96
+ {
97
+ "name" => cardinality.name.to_s,
98
+ "source" => range_data(cardinality.source_range),
99
+ "destination" => range_data(cardinality.destination_range)
100
+ }
101
+ end
102
+
103
+ def range_data(range)
104
+ {
105
+ "minimum" => range.begin,
106
+ "maximum" => range.end.finite? ? range.end : nil
107
+ }
108
+ end
109
+
110
+ def specialization_data(specialization)
111
+ generalized_id = specialization.generalized.name.to_s
112
+ specialized_id = specialization.specialized.name.to_s
113
+
114
+ {
115
+ "id" => endpoint_id("specialization", generalized_id, specialized_id),
116
+ "generalized_id" => generalized_id,
117
+ "specialized_id" => specialized_id,
118
+ "type" => specialization.inheritance? ? "inheritance" : "polymorphic"
119
+ }
120
+ end
121
+
122
+ def included_entity?(entity)
123
+ entity && @entity_ids.key?(entity.name.to_s)
124
+ end
125
+
126
+ def endpoint_id(kind, first_id, second_id)
127
+ "#{kind}:#{[first_id, second_id].sort.join('|')}"
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "stringio"
5
+
6
+ module CanvasERD
7
+ class DiagramStore
8
+ class Error < StandardError; end
9
+ class NotFound < Error; end
10
+
11
+ DEFAULT_DIRECTORY = "docs/erd"
12
+ FILENAME_PATTERN = /\A[^\x00\/\\]+\.erd\.png\z/i
13
+
14
+ attr_reader :directory
15
+
16
+ def initialize(root:, directory: DEFAULT_DIRECTORY)
17
+ @root = File.expand_path(root)
18
+ @directory = File.expand_path(directory, @root)
19
+ return if @directory.start_with?("#{@root}#{File::SEPARATOR}")
20
+
21
+ raise Error, "Diagram directory must be inside the Rails application"
22
+ end
23
+
24
+ def relative_directory
25
+ directory.delete_prefix("#{@root}#{File::SEPARATOR}")
26
+ end
27
+
28
+ def names
29
+ return [] unless Dir.exist?(directory)
30
+
31
+ Dir.children(directory).select { |name| valid_filename?(name) && File.file?(path_for(name)) }.sort
32
+ end
33
+
34
+ def read(name)
35
+ bytes = File.binread(path_for(name))
36
+ PngDocument.read(StringIO.new(bytes))
37
+ bytes
38
+ rescue Errno::ENOENT
39
+ raise NotFound, "Diagram not found: #{name}"
40
+ rescue SystemCallError => error
41
+ raise Error, "Could not read diagram #{name}: #{error.message}"
42
+ rescue PngDocument::Error => error
43
+ raise Error, error.message
44
+ end
45
+
46
+ def write(name, bytes)
47
+ path = path_for(name)
48
+ PngDocument.read(StringIO.new(bytes))
49
+ FileUtils.mkdir_p(directory)
50
+ File.binwrite(path, bytes)
51
+ name
52
+ rescue SystemCallError => error
53
+ raise Error, "Could not save diagram #{name}: #{error.message}"
54
+ rescue PngDocument::Error => error
55
+ raise Error, error.message
56
+ end
57
+
58
+ private
59
+
60
+ def path_for(name)
61
+ raise Error, "Diagram filename must end in .erd.png" unless valid_filename?(name)
62
+
63
+ File.join(directory, name)
64
+ end
65
+
66
+ def valid_filename?(name)
67
+ name.is_a?(String) && FILENAME_PATTERN.match?(name)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "zlib"
5
+
6
+ module CanvasERD
7
+ class PngDocument
8
+ class Error < StandardError; end
9
+
10
+ SIGNATURE = "\x89PNG\r\n\x1a\n".b
11
+ KEYWORD = "CanvasERD"
12
+ ITXT_PREFIX = "#{KEYWORD}\0\0\0\0\0".b
13
+ MAX_CHUNK_SIZE = 64 * 1024 * 1024
14
+
15
+ def self.load(path)
16
+ File.open(path, "rb") { |file| read(file) }
17
+ rescue SystemCallError => error
18
+ raise Error, "Could not read diagram #{path}: #{error.message}"
19
+ end
20
+
21
+ def self.read(input)
22
+ raise Error, "Not a PNG file" unless input.read(SIGNATURE.bytesize) == SIGNATURE
23
+
24
+ document_json = nil
25
+ found_end = false
26
+
27
+ until found_end
28
+ length_bytes = input.read(4)
29
+ raise Error, "PNG ended before its IEND chunk" unless length_bytes&.bytesize == 4
30
+
31
+ length = length_bytes.unpack1("N")
32
+ raise Error, "PNG chunk is too large" if length > MAX_CHUNK_SIZE
33
+
34
+ type = read_exact(input, 4)
35
+ data = read_exact(input, length)
36
+ checksum = read_exact(input, 4).unpack1("N")
37
+ raise Error, "PNG chunk checksum is invalid" unless Zlib.crc32(type + data) == checksum
38
+
39
+ if type == "iTXt" && data.start_with?(ITXT_PREFIX)
40
+ document_json = data.delete_prefix(ITXT_PREFIX).force_encoding(Encoding::UTF_8)
41
+ end
42
+ found_end = type == "IEND"
43
+ end
44
+
45
+ raise Error, "PNG does not contain an editable CanvasERD diagram" unless document_json
46
+
47
+ validate(JSON.parse(document_json))
48
+ rescue JSON::ParserError => error
49
+ raise Error, "CanvasERD diagram metadata is invalid: #{error.message}"
50
+ end
51
+
52
+ def self.read_exact(input, length)
53
+ value = input.read(length)
54
+ raise Error, "PNG chunk is truncated" unless value&.bytesize == length
55
+
56
+ value
57
+ end
58
+ private_class_method :read_exact
59
+
60
+ def self.validate(document)
61
+ schema = document["schema"] if document.is_a?(Hash)
62
+ state = document["state"] if document.is_a?(Hash)
63
+ valid = document.is_a?(Hash) &&
64
+ document["format"] == "canvas_erd" &&
65
+ document["version"] == 1 &&
66
+ schema.is_a?(Hash) &&
67
+ schema["entities"].is_a?(Array) &&
68
+ schema["relationships"].is_a?(Array) &&
69
+ schema["specializations"].is_a?(Array) &&
70
+ state.is_a?(Hash) &&
71
+ state["includedEntityIds"].is_a?(Array) &&
72
+ state["positions"].is_a?(Hash) &&
73
+ state["notes"].is_a?(Array)
74
+ raise Error, "CanvasERD diagram format is not supported" unless valid
75
+
76
+ document
77
+ end
78
+ private_class_method :validate
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanvasERD
4
+ class SchemaProvider
5
+ def initialize(models: -> { ActiveRecord::Base.descendants }, diagram: -> { CanvasERD::Diagram.create })
6
+ @models = models
7
+ @diagram = diagram
8
+ end
9
+
10
+ def call
11
+ @models.call.each(&:reset_column_information)
12
+ @diagram.call
13
+ end
14
+ end
15
+ end