├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rspec ├── .standard.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── schema_doctor.rb └── schema_doctor │ ├── analyzer.rb │ ├── exporter.rb │ ├── railtie.rb │ ├── schema_file.rb │ ├── tasks │ └── schemadoc.rake │ ├── utils.rb │ └── version.rb ├── schema_doctor.gemspec ├── sig └── schema_doctor.rbs └── spec ├── schema_doctor_spec.rb └── spec_helper.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.3.1' 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Run the default task 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | # For available configuration options, see: 2 | # https://github.com/standardrb/standard 3 | ruby_version: 3.0 4 | format: progress 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.0.X] 4 | 5 | - Debug releases 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in schema_doctor.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "rspec", "~> 3.0" 11 | 12 | gem "standard", "~> 1.3" 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | schema_doctor (0.0.4) 5 | activerecord (~> 7.0) 6 | asciidoctor (~> 2.0) 7 | railties (~> 7.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actionpack (7.1.3.4) 13 | actionview (= 7.1.3.4) 14 | activesupport (= 7.1.3.4) 15 | nokogiri (>= 1.8.5) 16 | racc 17 | rack (>= 2.2.4) 18 | rack-session (>= 1.0.1) 19 | rack-test (>= 0.6.3) 20 | rails-dom-testing (~> 2.2) 21 | rails-html-sanitizer (~> 1.6) 22 | actionview (7.1.3.4) 23 | activesupport (= 7.1.3.4) 24 | builder (~> 3.1) 25 | erubi (~> 1.11) 26 | rails-dom-testing (~> 2.2) 27 | rails-html-sanitizer (~> 1.6) 28 | activemodel (7.1.3.4) 29 | activesupport (= 7.1.3.4) 30 | activerecord (7.1.3.4) 31 | activemodel (= 7.1.3.4) 32 | activesupport (= 7.1.3.4) 33 | timeout (>= 0.4.0) 34 | activesupport (7.1.3.4) 35 | base64 36 | bigdecimal 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | connection_pool (>= 2.2.5) 39 | drb 40 | i18n (>= 1.6, < 2) 41 | minitest (>= 5.1) 42 | mutex_m 43 | tzinfo (~> 2.0) 44 | asciidoctor (2.0.23) 45 | ast (2.4.2) 46 | base64 (0.2.0) 47 | bigdecimal (3.1.8) 48 | builder (3.3.0) 49 | concurrent-ruby (1.3.3) 50 | connection_pool (2.4.1) 51 | crass (1.0.6) 52 | diff-lcs (1.5.1) 53 | drb (2.2.1) 54 | erubi (1.13.0) 55 | i18n (1.14.5) 56 | concurrent-ruby (~> 1.0) 57 | io-console (0.7.2) 58 | irb (1.13.2) 59 | rdoc (>= 4.0.0) 60 | reline (>= 0.4.2) 61 | json (2.7.2) 62 | language_server-protocol (3.17.0.3) 63 | lint_roller (1.1.0) 64 | loofah (2.22.0) 65 | crass (~> 1.0.2) 66 | nokogiri (>= 1.12.0) 67 | minitest (5.24.1) 68 | mutex_m (0.2.0) 69 | nokogiri (1.16.6-aarch64-linux) 70 | racc (~> 1.4) 71 | nokogiri (1.16.6-arm-linux) 72 | racc (~> 1.4) 73 | nokogiri (1.16.6-arm64-darwin) 74 | racc (~> 1.4) 75 | nokogiri (1.16.6-x86-linux) 76 | racc (~> 1.4) 77 | nokogiri (1.16.6-x86_64-darwin) 78 | racc (~> 1.4) 79 | nokogiri (1.16.6-x86_64-linux) 80 | racc (~> 1.4) 81 | parallel (1.25.1) 82 | parser (3.3.3.0) 83 | ast (~> 2.4.1) 84 | racc 85 | psych (5.1.2) 86 | stringio 87 | racc (1.8.0) 88 | rack (3.1.4) 89 | rack-session (2.0.0) 90 | rack (>= 3.0.0) 91 | rack-test (2.1.0) 92 | rack (>= 1.3) 93 | rackup (2.1.0) 94 | rack (>= 3) 95 | webrick (~> 1.8) 96 | rails-dom-testing (2.2.0) 97 | activesupport (>= 5.0.0) 98 | minitest 99 | nokogiri (>= 1.6) 100 | rails-html-sanitizer (1.6.0) 101 | loofah (~> 2.21) 102 | nokogiri (~> 1.14) 103 | railties (7.1.3.4) 104 | actionpack (= 7.1.3.4) 105 | activesupport (= 7.1.3.4) 106 | irb 107 | rackup (>= 1.0.0) 108 | rake (>= 12.2) 109 | thor (~> 1.0, >= 1.2.2) 110 | zeitwerk (~> 2.6) 111 | rainbow (3.1.1) 112 | rake (13.2.1) 113 | rdoc (6.7.0) 114 | psych (>= 4.0.0) 115 | regexp_parser (2.9.2) 116 | reline (0.5.9) 117 | io-console (~> 0.5) 118 | rexml (3.3.1) 119 | strscan 120 | rspec (3.13.0) 121 | rspec-core (~> 3.13.0) 122 | rspec-expectations (~> 3.13.0) 123 | rspec-mocks (~> 3.13.0) 124 | rspec-core (3.13.0) 125 | rspec-support (~> 3.13.0) 126 | rspec-expectations (3.13.1) 127 | diff-lcs (>= 1.2.0, < 2.0) 128 | rspec-support (~> 3.13.0) 129 | rspec-mocks (3.13.1) 130 | diff-lcs (>= 1.2.0, < 2.0) 131 | rspec-support (~> 3.13.0) 132 | rspec-support (3.13.1) 133 | rubocop (1.64.1) 134 | json (~> 2.3) 135 | language_server-protocol (>= 3.17.0) 136 | parallel (~> 1.10) 137 | parser (>= 3.3.0.2) 138 | rainbow (>= 2.2.2, < 4.0) 139 | regexp_parser (>= 1.8, < 3.0) 140 | rexml (>= 3.2.5, < 4.0) 141 | rubocop-ast (>= 1.31.1, < 2.0) 142 | ruby-progressbar (~> 1.7) 143 | unicode-display_width (>= 2.4.0, < 3.0) 144 | rubocop-ast (1.31.3) 145 | parser (>= 3.3.1.0) 146 | rubocop-performance (1.21.1) 147 | rubocop (>= 1.48.1, < 2.0) 148 | rubocop-ast (>= 1.31.1, < 2.0) 149 | ruby-progressbar (1.13.0) 150 | standard (1.39.0) 151 | language_server-protocol (~> 3.17.0.2) 152 | lint_roller (~> 1.0) 153 | rubocop (~> 1.64.0) 154 | standard-custom (~> 1.0.0) 155 | standard-performance (~> 1.4) 156 | standard-custom (1.0.2) 157 | lint_roller (~> 1.0) 158 | rubocop (~> 1.50) 159 | standard-performance (1.4.0) 160 | lint_roller (~> 1.1) 161 | rubocop-performance (~> 1.21.0) 162 | stringio (3.1.1) 163 | strscan (3.1.0) 164 | thor (1.3.1) 165 | timeout (0.4.1) 166 | tzinfo (2.0.6) 167 | concurrent-ruby (~> 1.0) 168 | unicode-display_width (2.5.0) 169 | webrick (1.8.1) 170 | zeitwerk (2.6.16) 171 | 172 | PLATFORMS 173 | aarch64-linux 174 | arm-linux 175 | arm64-darwin 176 | x86-linux 177 | x86_64-darwin 178 | x86_64-linux 179 | 180 | DEPENDENCIES 181 | rake (~> 13.0) 182 | rspec (~> 3.0) 183 | schema_doctor! 184 | standard (~> 1.3) 185 | 186 | BUNDLED WITH 187 | 2.5.9 188 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 @lni_T 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SchemaDoctor 2 | 3 | Automatic database documentation tool, for Ruby on Rails project. 4 | 5 | ## Demo 6 | 7 | - **[Sample Output](https://lnit.github.io/schema_doctor/)** 8 | - Generated from [Mastodon](https://github.com/mastodon/mastodon)'s [schema](https://github.com/mastodon/mastodon/blob/9be77fc0dbb01c1a8a54cd3da97e16c7941df367/db/schema.rb). 9 | 10 | ## Installation 11 | 12 | Add the gem to your project 13 | 14 | ```rb 15 | gem "rails" 16 | 17 | group :development do 18 | gem "schema_doctor" 19 | end 20 | ``` 21 | 22 | Then `bundle install` and you are ready to go. 23 | 24 | ## Usage 25 | 26 | ```sh 27 | rails schema:analyze # Analyze Database Schema. 28 | rails schema:export # Export HTML. 29 | ``` 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 "standard/rake" 9 | 10 | task default: %i[spec standard] 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "schema_doctor" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/schema_doctor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "schema_doctor/version" 4 | require_relative "schema_doctor/schema_file" 5 | require_relative "schema_doctor/analyzer" 6 | require_relative "schema_doctor/exporter" 7 | require_relative "schema_doctor/utils" 8 | require_relative "schema_doctor/railtie" if defined?(Rails::Railtie) 9 | 10 | module SchemaDoctor 11 | class Error < StandardError; end 12 | end 13 | -------------------------------------------------------------------------------- /lib/schema_doctor/analyzer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | 5 | module SchemaDoctor 6 | class Analyzer 7 | attr_reader :schema_file 8 | 9 | def initialize 10 | @schema_file = SchemaFile.new 11 | end 12 | 13 | def analyze_schema 14 | existing_schema = schema_file.load 15 | 16 | puts "== Analyzing model schema..." 17 | new_schema = model_schema(existing_schema || {}) 18 | 19 | puts "== Exporting Specification files..." 20 | schema_file.dump(new_schema) 21 | end 22 | 23 | def model_schema(schema = {}) 24 | models.each do |model| 25 | next if model.table_name.blank? 26 | 27 | puts "Processing #{model.name}..." 28 | schema[model.name.to_sym] = 29 | { 30 | name: model.name, 31 | table_name: model.table_name, 32 | table_comment: connection.table_comment(model.table_name), 33 | extra_comment: schema.dig(model.name, :extra_comment), 34 | columns: columns(model, schema.dig(model.name, :columns) || {}), 35 | indexes: indexes(model), 36 | associations: associations(model) 37 | } 38 | rescue ActiveRecord::TableNotSpecified 39 | nil 40 | rescue => e 41 | # Skip analyzing if an error occurs 42 | puts "Failed to process #{model.name}: #{e.inspect}" 43 | puts "\e[31mWe're sorry, Failed to process \e[33m#{model.name}\e[31m:\n #{e.inspect}\e[0m" 44 | end 45 | 46 | schema 47 | end 48 | 49 | private 50 | 51 | def connection 52 | @connection ||= ActiveRecord::Base.connection 53 | end 54 | 55 | def models 56 | return @models if @models 57 | 58 | Rails.application.eager_load! if defined?(Rails) 59 | @models = ActiveRecord::Base.descendants.sort_by!(&:name) 60 | end 61 | 62 | def columns(model, columns = {}) 63 | model.columns.each do |column| 64 | columns[column.name.to_sym] = 65 | { 66 | name: column.name, 67 | type: column.type, 68 | sql_type: column.sql_type, 69 | default: column.default, 70 | null: column.null, 71 | limit: column.limit, 72 | precision: column.precision, 73 | scale: column.scale, 74 | column_comment: column.comment, 75 | extra_comment: columns.dig(column.name, :extra_comment) 76 | } 77 | end 78 | columns 79 | end 80 | 81 | def indexes(model) 82 | connection.indexes(model.table_name).each_with_object({}) do |index, hash| 83 | hash[index.name.to_sym] = { 84 | name: index.name, 85 | columns: index.columns, 86 | unique: index.unique, 87 | using: index.using 88 | } 89 | end 90 | end 91 | 92 | def associations(model) 93 | result = { 94 | belongs: {}, 95 | has: {} 96 | } 97 | 98 | model.reflect_on_all_associations.each do |association| 99 | case association.macro.to_s 100 | when /\Abelongs_to/ 101 | result[:belongs][association.name.to_sym] = { 102 | macro: association.macro.to_s, 103 | name: association.name, 104 | class_name: association.class_name, 105 | foreign_key: association.foreign_key, 106 | options: Utils.safety_dump_hash(association.options), 107 | polymorphic: association.polymorphic? 108 | } 109 | when /\Ahas/ 110 | result[:has][association.name.to_sym] = { 111 | macro: association.macro.to_s, 112 | name: association.name, 113 | class_name: association.class_name, 114 | options: Utils.safety_dump_hash(association.options) 115 | } 116 | else 117 | puts "Unknown association type: #{association.macro}: :#{association.name}" 118 | next 119 | end 120 | end 121 | 122 | result 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/schema_doctor/exporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "asciidoctor" 4 | 5 | module SchemaDoctor 6 | class Exporter 7 | attr_reader :schema_file 8 | 9 | def initialize 10 | @schema_file = SchemaFile.new 11 | end 12 | 13 | def export_adoc 14 | puts "== Loading model specifications..." 15 | specs = schema_file.load 16 | 17 | puts "== Exporting adoc file..." 18 | # TODO: Use template 19 | File.open(adoc_path, "w") do |f| 20 | f.puts <<~TEXT 21 | = Rails Model Specification 22 | :toc: left 23 | :toclevels: 1 24 | :toc-title: Table of Contents 25 | :linkattrs: 26 | :sectlinks: 27 | :sectanchors: 28 | 29 | TEXT 30 | 31 | specs.each_value do |model| 32 | f.puts <<~TEXT 33 | == #{model[:name]} 34 | * Table name: #{model[:table_name]} 35 | * Comment: #{model[:table_comment]} 36 | #{model[:extra_comment]} 37 | 38 | === Columns 39 | [cols="1,1,1,1,1,1,1,1,2"] 40 | |=== 41 | |name|type|sql_type|default|null|limit|precision|scale|comment 42 | 43 | TEXT 44 | 45 | # Output columns schema 46 | model[:columns].each_value do |column| 47 | column[:column_comment] = column[:column_comment].to_s + column.delete(:extra_comment).to_s 48 | col_str = column.values.map do |v| 49 | escaped = v.to_s.gsub("|", "{vbar}") # "|" is table delimiter in asciidoc 50 | "|#{escaped}\n" 51 | end.join 52 | 53 | f.puts <<~TEXT 54 | #{col_str} 55 | TEXT 56 | end 57 | f.puts "|===" 58 | 59 | # Output indexes schema 60 | f.puts <<~TEXT 61 | 62 | === Indexes 63 | TEXT 64 | if model[:indexes].present? 65 | f.puts <<~TEXT 66 | [cols="2,2,1,1"] 67 | |=== 68 | |name|columns|unique|using 69 | 70 | TEXT 71 | 72 | model[:indexes].each_value do |index| 73 | index_str = index.values.map do |v| 74 | escaped = v.to_s.gsub("|", "{vbar}") # "|" is table delimiter in asciidoc 75 | "|#{escaped}\n" 76 | end.join 77 | 78 | f.puts <<~TEXT 79 | #{index_str} 80 | TEXT 81 | end 82 | 83 | f.puts "|===" 84 | else 85 | f.puts <<~TEXT 86 | None 87 | 88 | TEXT 89 | end 90 | 91 | # Output associations schema 92 | f.puts <<~TEXT 93 | 94 | === Associations 95 | TEXT 96 | belongs = model[:associations][:belongs] 97 | if belongs.present? 98 | f.puts <<~TEXT 99 | [cols="1,1,1,1"] 100 | |=== 101 | |macro|name|foreign_key|options 102 | 103 | TEXT 104 | 105 | belongs.each_value do |association| 106 | class_name = association.delete(:class_name).to_s 107 | polymorphic = association.delete(:polymorphic) 108 | # TODO: polymorphicのリンク先を特定できるようにする。今のところリンクを生成しないように。 109 | association[:name] = "link:#_#{class_name.downcase.gsub("::", "")}[#{association[:name]}]" unless polymorphic 110 | 111 | str = association.values.map do |v| 112 | escaped = v.to_s.gsub("|", "{vbar}") 113 | "|#{escaped}\n" 114 | end.join 115 | 116 | f.puts <<~TEXT 117 | #{str} 118 | TEXT 119 | end 120 | 121 | f.puts "|===" 122 | end 123 | 124 | has = model[:associations][:has] 125 | if has.present? 126 | f.puts <<~TEXT 127 | [cols="1,1,1"] 128 | |=== 129 | |macro|name|options 130 | 131 | TEXT 132 | 133 | has.each_value do |association| 134 | class_name = association.delete(:class_name).to_s 135 | association[:name] = "link:#_#{class_name.gsub("::", "").downcase}[#{association[:name]}]" 136 | str = association.values.map do |v| 137 | escaped = v.to_s.gsub("|", "{vbar}") 138 | "|#{escaped}\n" 139 | end.join 140 | 141 | f.puts <<~TEXT 142 | #{str} 143 | TEXT 144 | end 145 | 146 | f.puts "|===" 147 | end 148 | 149 | if belongs.empty? & has.empty? 150 | f.puts <<~TEXT 151 | None 152 | 153 | TEXT 154 | end 155 | end 156 | end 157 | puts "Done! => #{adoc_path}" 158 | end 159 | 160 | def convert_adoc_to_html 161 | puts "== Converting to html..." 162 | Asciidoctor.convert_file adoc_path, to_file: html_path, standalone: true, safe: :server 163 | puts "Done! => #{html_path}" 164 | end 165 | 166 | private 167 | 168 | def export_dir 169 | "docs/models" # TODO: Configurable 170 | end 171 | 172 | def adoc_path 173 | "#{export_dir}/models.adoc" # TODO: Configurable 174 | end 175 | 176 | def html_path 177 | "#{export_dir}/index.html" # TODO: Configurable 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/schema_doctor/railtie.rb: -------------------------------------------------------------------------------- 1 | module SchemaDoctor 2 | class Railtie < ::Rails::Railtie 3 | rake_tasks do 4 | load File.join(File.dirname(__FILE__), "tasks/schemadoc.rake") 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/schema_doctor/schema_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | 5 | module SchemaDoctor 6 | class SchemaFile 7 | def load 8 | return unless Dir.exist?(schema_dir) 9 | 10 | # Load schema specification files and merge them into a single hash 11 | Dir.glob("#{schema_dir}/*").inject({}) do |hash, file| 12 | hash.merge(YAML.load_file(file, symbolize_names: true)) 13 | end 14 | end 15 | 16 | def dump(specs) 17 | # Output specification files for each model 18 | FileUtils.mkdir_p(schema_dir) 19 | Utils.deep_stringify(specs).each do |name, spec| 20 | File.open("#{schema_dir}/#{name}.yml", "w") do |f| 21 | YAML.dump({name => spec}, f) 22 | end 23 | end 24 | end 25 | 26 | def delete 27 | FileUtils.rm_rf(schema_dir) 28 | end 29 | 30 | private 31 | 32 | def export_dir 33 | "docs/models" # TODO: Configurable 34 | end 35 | 36 | def schema_dir 37 | "#{export_dir}/specs" # TODO: Configurable 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/schema_doctor/tasks/schemadoc.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :schema do 4 | desc "Analyze schema and Export yml files" 5 | task analyze: :environment do 6 | SchemaDoctor::Analyzer.new.analyze_schema 7 | end 8 | 9 | desc "Export schema documentation" 10 | task export: :environment do 11 | exporter = SchemaDoctor::Exporter.new 12 | exporter.export_adoc 13 | exporter.convert_adoc_to_html 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/schema_doctor/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SchemaDoctor 4 | class Utils 5 | class << self 6 | def safety_dump_hash(obj) 7 | case obj 8 | when Hash 9 | obj.transform_values { |v| safety_dump_hash(v) } 10 | when Array 11 | obj.map { |v| safety_dump_hash(v) } 12 | when TrueClass, FalseClass, NilClass, Integer, Float, String 13 | obj 14 | else 15 | obj.to_s 16 | end 17 | end 18 | 19 | def deep_stringify(obj) 20 | case obj 21 | when Hash 22 | obj.transform_keys(&:to_s).transform_values { |v| deep_stringify(v) } 23 | when Array 24 | obj.map { |v| deep_stringify(v) } 25 | else 26 | obj 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/schema_doctor/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SchemaDoctor 4 | VERSION = "0.0.4" 5 | end 6 | -------------------------------------------------------------------------------- /schema_doctor.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/schema_doctor/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "schema_doctor" 7 | spec.version = SchemaDoctor::VERSION 8 | spec.authors = ["lni_T"] 9 | spec.email = ["developer.lni@gmail.com"] 10 | 11 | spec.summary = "Automatic database documentation tool." 12 | # spec.description = "TODO: Write a longer description or delete this line." 13 | spec.homepage = "https://github.com/lnit/schema_doctor" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 3.0.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | spec.metadata["changelog_uri"] = "https://github.com/lnit/schema_doctor/blob/master/CHANGELOG.md" 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | gemspec = File.basename(__FILE__) 24 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 25 | ls.readlines("\x0", chomp: true).reject do |f| 26 | (f == gemspec) || 27 | f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) 28 | end 29 | end 30 | spec.bindir = "exe" 31 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 32 | spec.require_paths = ["lib"] 33 | 34 | spec.add_dependency "activerecord", "~> 7.0" 35 | spec.add_dependency "railties", "~> 7.0" 36 | spec.add_dependency "asciidoctor", "~> 2.0" 37 | 38 | # For more information and examples about making a new gem, check out our 39 | # guide at: https://bundler.io/guides/creating_gem.html 40 | end 41 | -------------------------------------------------------------------------------- /sig/schema_doctor.rbs: -------------------------------------------------------------------------------- 1 | module SchemaDoctor 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /spec/schema_doctor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe SchemaDoctor do 4 | it "has a version number" do 5 | expect(SchemaDoctor::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "schema_doctor" 4 | 5 | RSpec.configure do |config| 6 | # Enable flags like --only-failures and --next-failure 7 | config.example_status_persistence_file_path = ".rspec_status" 8 | 9 | # Disable RSpec exposing methods globally on `Module` and `main` 10 | config.disable_monkey_patching! 11 | 12 | config.expect_with :rspec do |c| 13 | c.syntax = :expect 14 | end 15 | end 16 | --------------------------------------------------------------------------------