├── .gemtest ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── erd ├── examples ├── README ├── applications │ ├── event_forms │ │ ├── COPYRIGHT │ │ ├── models │ │ │ ├── event.rb │ │ │ ├── event_date.rb │ │ │ ├── form.rb │ │ │ ├── form_field_value.rb │ │ │ ├── form_fields.rb │ │ │ ├── group.rb │ │ │ ├── organization.rb │ │ │ ├── signup.rb │ │ │ └── stylesheet.rb │ │ └── schema.rb │ ├── gemcutter │ │ ├── MIT-LICENSE │ │ ├── models │ │ │ ├── dependency.rb │ │ │ ├── download.rb │ │ │ ├── linkset.rb │ │ │ ├── ownership.rb │ │ │ ├── rubygem.rb │ │ │ ├── subscription.rb │ │ │ ├── user.rb │ │ │ ├── version.rb │ │ │ └── web_hook.rb │ │ ├── options.rb │ │ └── schema.rb │ ├── refinery │ │ ├── LICENSE │ │ ├── lib │ │ │ └── has_friendly_id.rb │ │ ├── models │ │ │ ├── image.rb │ │ │ ├── inquiry.rb │ │ │ ├── inquiry_setting.rb │ │ │ ├── page.rb │ │ │ ├── page_part.rb │ │ │ ├── refinery_setting.rb │ │ │ ├── resource.rb │ │ │ ├── role.rb │ │ │ ├── slug.rb │ │ │ ├── user.rb │ │ │ └── user_plugin.rb │ │ ├── options.rb │ │ └── schema.rb │ ├── spree │ │ ├── LICENSE │ │ ├── lib │ │ │ └── delegate_belongs_to.rb │ │ ├── models │ │ │ ├── address.rb │ │ │ ├── adjustment.rb │ │ │ ├── app_configuration.rb │ │ │ ├── asset.rb │ │ │ ├── billing_integration.rb │ │ │ ├── calculator.rb │ │ │ ├── calculator │ │ │ │ ├── flat_percent_item_total.rb │ │ │ │ ├── flat_rate.rb │ │ │ │ ├── flexi_rate.rb │ │ │ │ ├── per_item.rb │ │ │ │ ├── price_bucket.rb │ │ │ │ ├── sales_tax.rb │ │ │ │ └── vat.rb │ │ │ ├── checkout.rb │ │ │ ├── configuration.rb │ │ │ ├── country.rb │ │ │ ├── creditcard.rb │ │ │ ├── gateway.rb │ │ │ ├── gateway │ │ │ │ ├── authorize_net.rb │ │ │ │ ├── authorize_net_cim.rb │ │ │ │ ├── beanstream.rb │ │ │ │ ├── bogus.rb │ │ │ │ ├── eway.rb │ │ │ │ ├── linkpoint.rb │ │ │ │ ├── pay_pal.rb │ │ │ │ └── sage_pay.rb │ │ │ ├── image.rb │ │ │ ├── inventory_unit.rb │ │ │ ├── line_item.rb │ │ │ ├── mail_method.rb │ │ │ ├── option_type.rb │ │ │ ├── option_value.rb │ │ │ ├── order.rb │ │ │ ├── payment.rb │ │ │ ├── payment_method.rb │ │ │ ├── payment_method │ │ │ │ └── check.rb │ │ │ ├── preference.rb │ │ │ ├── product.rb │ │ │ ├── product_group.rb │ │ │ ├── product_option_type.rb │ │ │ ├── product_property.rb │ │ │ ├── product_scope.rb │ │ │ ├── property.rb │ │ │ ├── prototype.rb │ │ │ ├── return_authorization.rb │ │ │ ├── role.rb │ │ │ ├── shipment.rb │ │ │ ├── shipping_category.rb │ │ │ ├── shipping_method.rb │ │ │ ├── state.rb │ │ │ ├── state_event.rb │ │ │ ├── tax_category.rb │ │ │ ├── tax_rate.rb │ │ │ ├── taxon.rb │ │ │ ├── taxonomy.rb │ │ │ ├── tracker.rb │ │ │ ├── user.rb │ │ │ ├── variant.rb │ │ │ ├── zone.rb │ │ │ └── zone_member.rb │ │ ├── options.rb │ │ └── schema.rb │ └── typo │ │ ├── MIT-LICENSE │ │ ├── models │ │ ├── article.rb │ │ ├── blog.rb │ │ ├── categorization.rb │ │ ├── category.rb │ │ ├── comment.rb │ │ ├── content.rb │ │ ├── feedback.rb │ │ ├── notification.rb │ │ ├── page.rb │ │ ├── ping.rb │ │ ├── profile.rb │ │ ├── redirect.rb │ │ ├── resource.rb │ │ ├── right.rb │ │ ├── sidebar.rb │ │ ├── tag.rb │ │ ├── text_filter.rb │ │ ├── trackback.rb │ │ ├── trigger.rb │ │ └── user.rb │ │ ├── options.rb │ │ └── schema.rb ├── associations │ ├── many-to-many-indirect │ │ ├── models │ │ │ ├── spell.rb │ │ │ ├── spell_mastery.rb │ │ │ └── wizard.rb │ │ ├── options.rb │ │ └── schema.rb │ ├── many-to-many │ │ ├── models │ │ │ ├── film.rb │ │ │ └── genre.rb │ │ ├── options.rb │ │ └── schema.rb │ ├── one-to-many │ │ ├── models │ │ │ ├── cannon.rb │ │ │ └── galleon.rb │ │ ├── options.rb │ │ └── schema.rb │ ├── one-to-one-recursive │ │ ├── models │ │ │ └── emperor.rb │ │ ├── options.rb │ │ └── schema.rb │ └── one-to-one │ │ ├── models │ │ ├── country.rb │ │ └── head_of_state.rb │ │ ├── options.rb │ │ └── schema.rb ├── domains │ ├── orchard-company-orchard │ │ ├── models │ │ │ ├── company.rb │ │ │ └── orchard.rb │ │ ├── options.rb │ │ └── schema.rb │ ├── orchard-orchard-stand │ │ ├── models │ │ │ ├── orchard.rb │ │ │ └── stand.rb │ │ ├── options.rb │ │ └── schema.rb │ └── orchard │ │ ├── models │ │ ├── company.rb │ │ ├── orchard.rb │ │ ├── picking_robot.rb │ │ ├── species.rb │ │ ├── stand.rb │ │ └── tree.rb │ │ ├── options.rb │ │ └── schema.rb ├── entities │ └── attributes │ │ ├── models │ │ └── photograph.rb │ │ ├── options.rb │ │ └── schema.rb ├── erdconfig.another_example ├── erdconfig.example ├── generate.rb ├── identity.rb ├── inheritance │ └── single-inheritance │ │ ├── models │ │ ├── beer.rb │ │ ├── beverage.rb │ │ └── whisky.rb │ │ ├── options.rb │ │ └── schema.rb ├── meta │ └── rails-erd │ │ ├── models │ │ ├── cardinality.rb │ │ ├── domain.rb │ │ ├── entity.rb │ │ ├── property.rb │ │ ├── relationship.rb │ │ └── specialization.rb │ │ ├── options.rb │ │ └── schema.rb ├── polymorphism │ └── polymorphic-belongs-to │ │ ├── models │ │ ├── barricade.rb │ │ ├── soldier.rb │ │ └── stronghold.rb │ │ ├── options.rb │ │ └── schema.rb └── sfdp.rb ├── gemfiles ├── Gemfile-rails.4.2.x ├── Gemfile-rails.5.0.x ├── Gemfile-rails.5.1.x ├── Gemfile-rails.5.2.x ├── Gemfile-rails.6.0.x ├── Gemfile-rails.6.1.x ├── Gemfile-rails.7.0.x └── Gemfile-rails.edge ├── lib ├── generators │ └── erd │ │ ├── USAGE │ │ ├── install_generator.rb │ │ └── templates │ │ └── auto_generate_diagram.rake ├── rails-erd.rb ├── rails_erd.rb ├── rails_erd │ ├── cli.rb │ ├── config.rb │ ├── diagram.rb │ ├── diagram │ │ ├── graphviz.rb │ │ ├── mermaid.rb │ │ └── templates │ │ │ ├── node.html.erb │ │ │ └── node.record.erb │ ├── domain.rb │ ├── domain │ │ ├── attribute.rb │ │ ├── entity.rb │ │ ├── relationship.rb │ │ ├── relationship │ │ │ └── cardinality.rb │ │ └── specialization.rb │ ├── railtie.rb │ ├── tasks.rake │ └── version.rb └── tasks │ └── auto_generate_diagram.rake ├── rails-erd.gemspec └── test ├── support_files ├── erdconfig.another_example ├── erdconfig.example └── erdconfig.exclude.example ├── test_helper.rb └── unit ├── attribute_test.rb ├── cardinality_test.rb ├── config_test.rb ├── diagram_test.rb ├── domain_test.rb ├── entity_test.rb ├── graphviz_test.rb ├── mermaid_test.rb ├── rake_task_test.rb ├── relationship_test.rb └── specialization_test.rb /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voormedia/rails-erd/7c66258b6818c47b4d878c2ad7ff6decebdf834a/.gemtest -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-18.04 13 | strategy: 14 | matrix: 15 | ruby_version: ['3.1', '3.0', '2.7'] 16 | rails_version: ['6.0.x', '6.1.x', '7.0.x', 'edge'] 17 | name: Ruby ${{ matrix.ruby_version }} on Rails ${{ matrix.rails_version }} 18 | env: 19 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/Gemfile-rails.${{ matrix.rails_version }} 20 | steps: 21 | - name: Install graphviz 22 | run: | 23 | sudo apt-get update -qq 24 | sudo apt-get install -qq graphviz 25 | - uses: actions/checkout@v3 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby_version }} 29 | bundler-cache: true 30 | - name: Run tests 31 | run: | 32 | bundle exec rake test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bundle 3 | .yardoc 4 | *.pdf 5 | *.rbc 6 | .rvmrc 7 | doc 8 | pkg 9 | output 10 | rdoc 11 | site/_generated 12 | Gemfile.lock 13 | gemfiles/*.lock 14 | .idea/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2 4 | - 2.3 5 | - 2.4 6 | - 2.5 7 | - 2.6 8 | - 2.7 9 | - 3.0 10 | - 3.1 11 | - jruby 12 | gemfile: 13 | - gemfiles/Gemfile-rails.4.2.x 14 | - gemfiles/Gemfile-rails.5.0.x 15 | - gemfiles/Gemfile-rails.5.1.x 16 | - gemfiles/Gemfile-rails.5.2.x 17 | - gemfiles/Gemfile-rails.6.0.x 18 | - gemfiles/Gemfile-rails.6.1.x 19 | - gemfiles/Gemfile-rails.7.0.x 20 | - gemfiles/Gemfile-rails.edge 21 | before_install: 22 | - gem install bundler -v '< 2' 23 | - sudo apt-get update -qq 24 | - sudo apt-get install -qq graphviz 25 | script: bundle exec rake 26 | matrix: 27 | allow_failures: 28 | - rvm: jruby 29 | - gemfile: gemfiles/Gemfile-rails.edge 30 | fast_finish: true 31 | exclude: 32 | - rvm: 2.2 33 | gemfile: gemfiles/Gemfile-rails.6.0.x 34 | - rvm: 2.2 35 | gemfile: gemfiles/Gemfile-rails.6.1.x 36 | - rvm: 2.2 37 | gemfile: gemfiles/Gemfile-rails.7.0.x 38 | - rvm: 2.2 39 | gemfile: gemfiles/Gemfile-rails.edge 40 | - rvm: 2.3 41 | gemfile: gemfiles/Gemfile-rails.6.0.x 42 | - rvm: 2.3 43 | gemfile: gemfiles/Gemfile-rails.6.1.x 44 | - rvm: 2.3 45 | gemfile: gemfiles/Gemfile-rails.7.0.x 46 | - rvm: 2.3 47 | gemfile: gemfiles/Gemfile-rails.edge 48 | - rvm: 2.4 49 | gemfile: gemfiles/Gemfile-rails.6.0.x 50 | - rvm: 2.4 51 | gemfile: gemfiles/Gemfile-rails.6.1.x 52 | - rvm: 2.4 53 | gemfile: gemfiles/Gemfile-rails.7.0.x 54 | - rvm: 2.4 55 | gemfile: gemfiles/Gemfile-rails.edge 56 | cache: bundler 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 12 | 13 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 14 | 15 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | if ENV["edge"] 6 | gem "activerecord", :github => "rails/rails" 7 | end 8 | 9 | 10 | group :development, :test do 11 | gem 'minitest', '~> 5.14.0' 12 | end 13 | 14 | group :development do 15 | gem 'mocha' 16 | gem "rake" 17 | gem "yard" 18 | 19 | platforms :ruby do 20 | gem "activerecord", "< 7.0" 21 | gem "activesupport", "< 7.0" 22 | gem "sqlite3", '~> 1.4' 23 | gem "redcarpet" 24 | 25 | if RUBY_VERSION > "2.1.0" 26 | gem "test-unit" # not bundled in CRuby since 2.2.0 27 | end 28 | end 29 | 30 | platforms :jruby do 31 | gem "activerecord-jdbcsqlite3-adapter" 32 | gem "jruby-openssl", :require => false # Silence openssl warnings. 33 | end 34 | end -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2015 Voormedia B.V. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rails ERD - Generate Entity-Relationship Diagrams for Rails applications 2 | ======================================================================== 3 | [](https://github.com/voormedia/rails-erd/actions/workflows/test.yml) [](https://codeclimate.com/github/voormedia/rails-erd) 4 | 5 | [Rails ERD](https://voormedia.github.io/rails-erd/) is a gem that allows you to easily generate a diagram based on your application's Active Record models. The diagram gives an overview of how your models are related. Having a diagram that describes your models is perfect documentation for your application. 6 | 7 | The second goal of Rails ERD is to provide you with a tool to inspect your application's domain model. If you don't like the default output, it is very easy to use the API to build your own diagrams. 8 | 9 | Rails ERD was created specifically for Rails and works on versions 3.0-5.0. It uses Active Record's built-in reflection capabilities to figure out how your models are associated. 10 | 11 | 12 | Preview 13 | ------- 14 | 15 | Here's an example entity-relationship diagram that was generated by Rails ERD: 16 | 17 |  18 | 19 | Browse the [gallery](https://voormedia.github.io/rails-erd/gallery.html) for more example diagrams. 20 | 21 | 22 | Requirements 23 | --------------- 24 | 25 | * Ruby 1.9.3+ 26 | * ActiveRecord 3.x - 5.0.x 27 | 28 | Getting started 29 | --------------- 30 | 31 | See the [installation instructions](https://voormedia.github.io/rails-erd/install.html) for a complete description of how to install Rails ERD. Here's a summary: 32 | 33 | * Install Graphviz 2.22+ ([how?](https://voormedia.github.io/rails-erd/install.html)). On macOS with Homebrew run `brew install graphviz`. 34 | 35 | * on linux - `sudo apt-get install graphviz` 36 | 37 | * Add gem 'rails-erd', group: :development to your application's Gemfile 38 | 39 | * Run bundle exec erd 40 | 41 | ### Configuration 42 | 43 | 44 | Rails ERD has the ability to be configured via the command line or through the use of a YAML file with configuration options set. It will look for this file first at `~/.erdconfig` and then `./.erdconfig` (which will override any settings in `~/.erdconfig`). The format of the file is as follows (shown here with the default settings used if no `.erdconfig` is found). More information on [customization options](https://voormedia.github.io/rails-erd/customise.html) can be found in Rails ERD's project documentation. 45 | 46 | ```yaml 47 | attributes: 48 | - content 49 | - foreign_keys 50 | - inheritance 51 | disconnected: true 52 | filename: erd 53 | filetype: pdf 54 | indirect: true 55 | inheritance: false 56 | markup: true 57 | notation: simple 58 | orientation: horizontal 59 | polymorphism: false 60 | sort: true 61 | warn: true 62 | title: sample title 63 | exclude: null 64 | only: null 65 | only_recursion_depth: null 66 | prepend_primary: false 67 | cluster: false 68 | splines: spline 69 | fonts: 70 | normal: "Arial" 71 | bold: "Arial Bold" 72 | italic: "Arial Italic" 73 | ``` 74 | 75 | Auto generation 76 | --------------- 77 | 78 | * Run bundle exec rails g erd:install 79 | * Run bundle exec rails db:migrate, then the diagram is generated 80 | 81 | Learn more 82 | ---------- 83 | 84 | More information can be found on [Rails ERD's project homepage](https://voormedia.github.io/rails-erd/). 85 | 86 | If you wish to extend or customise Rails ERD, take a look at the [API documentation](http://rubydoc.info/github/voormedia/rails-erd/frames). 87 | 88 | 89 | About Rails ERD 90 | --------------- 91 | 92 | Rails ERD was created by Rolf Timmermans (r.timmermans *at* voormedia.com) 93 | 94 | Copyright 2010-2015 Voormedia - [www.voormedia.com](http://www.voormedia.com/) 95 | 96 | 97 | License 98 | ------- 99 | 100 | Rails ERD is released under the MIT license. 101 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | require "rake/testtask" 3 | require "yard" 4 | 5 | Bundler::GemHelper.install_tasks 6 | 7 | Rake::TestTask.new do |test| 8 | test.test_files = FileList["test/**/*_test.rb"] 9 | end 10 | 11 | YARD::Rake::YardocTask.new do |yard| 12 | yard.files = ["lib/**/*.rb", "-", "LICENSE", "CHANGES.md"] 13 | end 14 | 15 | desc "Generate diagrams for bundled examples" 16 | task :examples do 17 | require File.expand_path("examples/generate", File.dirname(__FILE__)) 18 | end 19 | 20 | task :default => :test 21 | -------------------------------------------------------------------------------- /bin/erd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rails_erd/cli" 3 | 4 | RailsERD::CLI.start 5 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | This directory contains some examples of domain models that can be displayed 2 | in a diagram with Rails ERD. Some domain models are copied from actual 3 | applications. Copyright on those models and the database schemas belongs to 4 | their respective authors. Please see the LICENSE files in each of the 5 | application's directories for details about the author's copyright and the 6 | terms under which the code has been licensed. 7 | -------------------------------------------------------------------------------- /examples/applications/event_forms/COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010 Voormedia - www.voormedia.com 2 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < ActiveRecord::Base 2 | belongs_to :group 3 | has_many :dates, :class_name => "EventDate", :foreign_key => "event_id" 4 | 5 | validates_presence_of :group, :title 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/event_date.rb: -------------------------------------------------------------------------------- 1 | class EventDate < ActiveRecord::Base 2 | belongs_to :event 3 | has_many :signups 4 | 5 | validates_presence_of :event, :expiry_date, :date 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/form.rb: -------------------------------------------------------------------------------- 1 | class Form < ActiveRecord::Base 2 | belongs_to :organization 3 | has_many :groups 4 | has_many :fields, :class_name => "FormField", :foreign_key => "form_id" 5 | 6 | validates_presence_of :name 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/form_field_value.rb: -------------------------------------------------------------------------------- 1 | class FormFieldValue < ActiveRecord::Base 2 | belongs_to :form_field 3 | 4 | validates_presence_of :key 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/form_fields.rb: -------------------------------------------------------------------------------- 1 | class FormField < ActiveRecord::Base 2 | belongs_to :form 3 | has_many :values, :class_name => "FormFieldValue", :foreign_key => "form_field_id" 4 | 5 | validates_presence_of :name 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/group.rb: -------------------------------------------------------------------------------- 1 | class Group < ActiveRecord::Base 2 | belongs_to :organization 3 | belongs_to :stylesheet 4 | belongs_to :form 5 | has_many :events 6 | has_many :event_dates, :through => :events, :source => :dates 7 | 8 | validates_presence_of :organization, :title, :url_slug, :form, :stylesheet 9 | end 10 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/organization.rb: -------------------------------------------------------------------------------- 1 | class Organization < ActiveRecord::Base 2 | has_many :groups 3 | has_many :stylesheets 4 | has_many :forms 5 | has_many :events, :through => :groups 6 | 7 | validates_presence_of :name, :subdomain 8 | end -------------------------------------------------------------------------------- /examples/applications/event_forms/models/signup.rb: -------------------------------------------------------------------------------- 1 | class Signup < ActiveRecord::Base 2 | belongs_to :event_date 3 | 4 | validates_presence_of :email 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/event_forms/models/stylesheet.rb: -------------------------------------------------------------------------------- 1 | class Stylesheet < ActiveRecord::Base 2 | belongs_to :organization 3 | has_many :groups 4 | 5 | validates_presence_of :name 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/event_forms/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 20100905230322) do 2 | create_table "event_dates", :force => true do |t| 3 | t.string "date" 4 | t.text "description" 5 | t.string "location" 6 | t.date "expiry_date" 7 | t.datetime "created_at" 8 | t.datetime "updated_at" 9 | t.integer "event_id" 10 | end 11 | 12 | create_table "events", :force => true do |t| 13 | t.string "title" 14 | t.text "description" 15 | t.text "introduction" 16 | t.text "report" 17 | t.string "speaker" 18 | t.string "target_audience" 19 | t.string "tutors" 20 | t.string "costs" 21 | t.string "duration" 22 | t.boolean "active" 23 | t.datetime "created_at" 24 | t.datetime "updated_at" 25 | t.integer "group_id" 26 | end 27 | 28 | create_table "form_fields", :force => true do |t| 29 | t.string "name" 30 | t.string "label" 31 | t.string "field_type" 32 | t.boolean "mandatory" 33 | t.datetime "created_at" 34 | t.datetime "updated_at" 35 | t.integer "form_id" 36 | end 37 | 38 | create_table "form_field_values", :force => true do |t| 39 | t.string "key" 40 | t.string "value" 41 | t.datetime "created_at" 42 | t.datetime "updated_at" 43 | t.integer "form_field_id" 44 | end 45 | 46 | create_table "forms", :force => true do |t| 47 | t.string "name" 48 | t.datetime "created_at" 49 | t.datetime "updated_at" 50 | t.integer "organization_id" 51 | end 52 | 53 | create_table "groups", :force => true do |t| 54 | t.string "title" 55 | t.string "url_slug" 56 | t.text "description" 57 | t.boolean "active" 58 | t.string "email_subject", :limit => 150 59 | t.text "email_message" 60 | t.string "email_receiver" 61 | t.integer "organization_id" 62 | t.integer "stylesheet_id" 63 | t.integer "form_id" 64 | t.datetime "created_at" 65 | t.datetime "updated_at" 66 | end 67 | 68 | create_table "organizations", :force => true do |t| 69 | t.string "name", :null => false 70 | t.string "subdomain", :null => false 71 | t.string "website" 72 | t.string "domain" 73 | t.string "phone" 74 | t.string "signup_title", :limit => 50 75 | t.string "email_subject", :limit => 150 76 | t.text "email_message" 77 | t.string "email_receiver" 78 | t.datetime "created_at" 79 | t.datetime "updated_at" 80 | end 81 | 82 | create_table "signups", :force => true do |t| 83 | t.string "email" 84 | t.text "serialized_fields" 85 | t.boolean "confirmed" 86 | t.integer "event_date_id" 87 | t.datetime "created_at" 88 | t.datetime "updated_at" 89 | end 90 | 91 | create_table "stylesheets", :force => true do |t| 92 | t.string "name" 93 | t.text "content" 94 | t.integer "organization_id" 95 | t.datetime "created_at" 96 | t.datetime "updated_at" 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Nick Quaranto 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/dependency.rb: -------------------------------------------------------------------------------- 1 | class Dependency < ActiveRecord::Base 2 | belongs_to :rubygem 3 | belongs_to :version 4 | 5 | validates_presence_of :requirements 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/download.rb: -------------------------------------------------------------------------------- 1 | class Download 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/linkset.rb: -------------------------------------------------------------------------------- 1 | class Linkset < ActiveRecord::Base 2 | belongs_to :rubygem 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/ownership.rb: -------------------------------------------------------------------------------- 1 | class Ownership < ActiveRecord::Base 2 | belongs_to :rubygem 3 | belongs_to :user 4 | 5 | validates_uniqueness_of :user_id, :scope => :rubygem_id 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/rubygem.rb: -------------------------------------------------------------------------------- 1 | class Rubygem < ActiveRecord::Base 2 | has_many :owners, :through => :ownerships, :source => :user 3 | has_many :ownerships, :dependent => :destroy 4 | has_many :subscribers, :through => :subscriptions, :source => :user 5 | has_many :subscriptions, :dependent => :destroy 6 | has_many :versions, :dependent => :destroy 7 | has_many :web_hooks, :dependent => :destroy 8 | has_one :linkset, :dependent => :destroy 9 | 10 | validates_presence_of :name 11 | validates_uniqueness_of :name 12 | end 13 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/subscription.rb: -------------------------------------------------------------------------------- 1 | class Subscription < ActiveRecord::Base 2 | belongs_to :rubygem 3 | belongs_to :user 4 | 5 | validates_uniqueness_of :rubygem_id, :scope => :user_id 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | if ActiveRecord::VERSION::MAJOR >= 4 3 | has_many :rubygems, 4 | lambda { where(:ownerships => { :approved => true }).order(:name => :asc) }, 5 | :through => :ownerships 6 | has_many :subscribed_gems, 7 | lambda { order(:name => :asc) }, 8 | :through => :subscriptions, 9 | :source => :rubygem 10 | else 11 | has_many :rubygems, :through => :ownerships, 12 | :order => "name ASC", 13 | :conditions => { 'ownerships.approved' => true } 14 | has_many :subscribed_gems, :through => :subscriptions, 15 | :source => :rubygem, 16 | :order => "name ASC" 17 | end 18 | has_many :ownerships 19 | has_many :subscriptions 20 | has_many :web_hooks 21 | 22 | validates_uniqueness_of :handle, :allow_nil => true 23 | end 24 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/version.rb: -------------------------------------------------------------------------------- 1 | class Version < ActiveRecord::Base 2 | belongs_to :rubygem 3 | has_many :dependencies, :dependent => :destroy 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/models/web_hook.rb: -------------------------------------------------------------------------------- 1 | class WebHook < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :rubygem 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/options.rb: -------------------------------------------------------------------------------- 1 | { :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/applications/gemcutter/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 20100817182653) do 2 | 3 | create_table "dependencies", :force => true do |t| 4 | t.string "requirements" 5 | t.datetime "created_at" 6 | t.datetime "updated_at" 7 | t.integer "rubygem_id" 8 | t.integer "version_id" 9 | t.string "scope" 10 | end 11 | 12 | add_index "dependencies", ["rubygem_id"], :name => "index_dependencies_on_rubygem_id" 13 | add_index "dependencies", ["version_id"], :name => "index_dependencies_on_version_id" 14 | 15 | create_table "downloads", :force => true do |t| 16 | t.integer "version_id" 17 | t.datetime "created_at" 18 | t.datetime "updated_at" 19 | end 20 | 21 | add_index "downloads", ["version_id"], :name => "index_downloads_on_version_id" 22 | 23 | create_table "linksets", :force => true do |t| 24 | t.integer "rubygem_id" 25 | t.string "home" 26 | t.string "wiki" 27 | t.string "docs" 28 | t.string "mail" 29 | t.string "code" 30 | t.string "bugs" 31 | t.datetime "created_at" 32 | t.datetime "updated_at" 33 | end 34 | 35 | add_index "linksets", ["rubygem_id"], :name => "index_linksets_on_rubygem_id" 36 | 37 | create_table "ownerships", :force => true do |t| 38 | t.integer "rubygem_id" 39 | t.integer "user_id" 40 | t.string "token" 41 | t.boolean "approved", :default => false 42 | t.datetime "created_at" 43 | t.datetime "updated_at" 44 | end 45 | 46 | add_index "ownerships", ["rubygem_id"], :name => "index_ownerships_on_rubygem_id" 47 | add_index "ownerships", ["user_id"], :name => "index_ownerships_on_user_id" 48 | 49 | create_table "rubyforgers", :force => true do |t| 50 | t.string "email" 51 | t.string "encrypted_password", :limit => 40 52 | end 53 | 54 | create_table "rubygems", :force => true do |t| 55 | t.string "name" 56 | t.datetime "created_at" 57 | t.datetime "updated_at" 58 | t.integer "downloads", :default => 0 59 | t.string "slug" 60 | end 61 | 62 | add_index "rubygems", ["name"], :name => "index_rubygems_on_name" 63 | 64 | create_table "subscriptions", :force => true do |t| 65 | t.integer "rubygem_id" 66 | t.integer "user_id" 67 | t.datetime "created_at" 68 | t.datetime "updated_at" 69 | end 70 | 71 | add_index "subscriptions", ["rubygem_id"], :name => "index_subscriptions_on_rubygem_id" 72 | add_index "subscriptions", ["user_id"], :name => "index_subscriptions_on_user_id" 73 | 74 | create_table "users", :force => true do |t| 75 | t.string "email" 76 | t.string "encrypted_password", :limit => 128 77 | t.string "salt", :limit => 128 78 | t.string "token", :limit => 128 79 | t.datetime "token_expires_at" 80 | t.boolean "email_confirmed", :default => false, :null => false 81 | t.string "api_key" 82 | t.string "confirmation_token", :limit => 128 83 | t.string "remember_token", :limit => 128 84 | t.datetime "created_at" 85 | t.datetime "updated_at" 86 | t.boolean "email_reset" 87 | t.string "handle" 88 | end 89 | 90 | add_index "users", ["email"], :name => "index_users_on_email" 91 | add_index "users", ["handle"], :name => "index_users_on_handle" 92 | add_index "users", ["id", "confirmation_token"], :name => "index_users_on_id_and_confirmation_token" 93 | add_index "users", ["id", "token"], :name => "index_users_on_id_and_token" 94 | add_index "users", ["remember_token"], :name => "index_users_on_remember_token" 95 | add_index "users", ["token"], :name => "index_users_on_token" 96 | 97 | create_table "versions", :force => true do |t| 98 | t.text "authors" 99 | t.text "description" 100 | t.string "number" 101 | t.integer "rubygem_id" 102 | t.datetime "built_at" 103 | t.datetime "updated_at" 104 | t.string "rubyforge_project" 105 | t.text "summary" 106 | t.string "platform" 107 | t.datetime "created_at" 108 | t.boolean "indexed", :default => true 109 | t.boolean "prerelease" 110 | t.integer "position" 111 | t.integer "downloads_count", :default => 0 112 | t.boolean "latest" 113 | t.string "full_name" 114 | end 115 | 116 | add_index "versions", ["built_at"], :name => "index_versions_on_built_at" 117 | add_index "versions", ["created_at"], :name => "index_versions_on_created_at" 118 | add_index "versions", ["full_name"], :name => "index_versions_on_full_name" 119 | add_index "versions", ["indexed"], :name => "index_versions_on_indexed" 120 | add_index "versions", ["number"], :name => "index_versions_on_number" 121 | add_index "versions", ["position"], :name => "index_versions_on_position" 122 | add_index "versions", ["prerelease"], :name => "index_versions_on_prerelease" 123 | add_index "versions", ["rubygem_id"], :name => "index_versions_on_rubygem_id" 124 | 125 | create_table "web_hooks", :force => true do |t| 126 | t.integer "user_id" 127 | t.string "url" 128 | t.integer "failure_count", :default => 0 129 | t.datetime "created_at" 130 | t.datetime "updated_at" 131 | t.integer "rubygem_id" 132 | end 133 | 134 | end 135 | -------------------------------------------------------------------------------- /examples/applications/refinery/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2005-2010 Resolve Digital (https://resolve.digital/) 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. -------------------------------------------------------------------------------- /examples/applications/refinery/lib/has_friendly_id.rb: -------------------------------------------------------------------------------- 1 | # Extracted from https://github.com/eric/friendly_id 2 | class ActiveRecord::Base 3 | def self.has_friendly_id(column, options = {}) 4 | if options[:use_slug] 5 | if ActiveRecord::VERSION::MAJOR >= 4 6 | has_many :slugs, lambda { order(:id => :desc) }, :as => :sluggable, :dependent => :destroy 7 | else 8 | has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy, :readonly => true 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/image.rb: -------------------------------------------------------------------------------- 1 | class Image < ActiveRecord::Base 2 | validates :image, :presence => true, 3 | :length => { :maximum => 20000000 } 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/inquiry.rb: -------------------------------------------------------------------------------- 1 | class Inquiry < ActiveRecord::Base 2 | validates :name, :presence => true 3 | validates :message, :presence => true 4 | validates :email, :format=> { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/inquiry_setting.rb: -------------------------------------------------------------------------------- 1 | class InquirySetting < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/page.rb: -------------------------------------------------------------------------------- 1 | class Page < ActiveRecord::Base 2 | validates :title, :presence => true 3 | has_friendly_id :title, :use_slug => true, 4 | :reserved_words => %w(index new session login logout users refinery admin images wymiframe) 5 | if ActiveRecord::VERSION::MAJOR >= 4 6 | has_many :parts, 7 | lambda { order(:position => :asc) }, 8 | :class_name => "PagePart", 9 | :inverse_of => :page, 10 | :dependent => :destroy 11 | else 12 | has_many :parts, 13 | :class_name => "PagePart", 14 | :order => "position ASC", 15 | :inverse_of => :page, 16 | :dependent => :destroy 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/page_part.rb: -------------------------------------------------------------------------------- 1 | class PagePart < ActiveRecord::Base 2 | belongs_to :page 3 | 4 | validates :title, :presence => true 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/refinery_setting.rb: -------------------------------------------------------------------------------- 1 | class RefinerySetting < ActiveRecord::Base 2 | validates :name, :presence => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/resource.rb: -------------------------------------------------------------------------------- 1 | class Resource < ActiveRecord::Base 2 | validates :file, :presence => true, 3 | :length => { :maximum => 50000000 } 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/role.rb: -------------------------------------------------------------------------------- 1 | class Role < ActiveRecord::Base 2 | has_and_belongs_to_many :users 3 | 4 | validates :title, :uniqueness => true 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/slug.rb: -------------------------------------------------------------------------------- 1 | # Extracted from https://github.com/eric/friendly_id 2 | class Slug < ActiveRecord::Base 3 | belongs_to :sluggable, :polymorphic => true 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_and_belongs_to_many :roles 3 | if ActiveRecord::VERSION::MAJOR >= 4 4 | has_many :plugins, lambda { order(:position => :asc) }, :class_name => "UserPlugin", :dependent => :destroy 5 | else 6 | has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy 7 | end 8 | has_friendly_id :login, :use_slug => true 9 | end 10 | -------------------------------------------------------------------------------- /examples/applications/refinery/models/user_plugin.rb: -------------------------------------------------------------------------------- 1 | class UserPlugin < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/refinery/options.rb: -------------------------------------------------------------------------------- 1 | { :disconnected => false, :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/applications/refinery/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 20100929035252) do 2 | 3 | create_table "images", :force => true do |t| 4 | t.string "image_mime_type" 5 | t.string "image_name" 6 | t.integer "image_size" 7 | t.integer "image_width" 8 | t.integer "image_height" 9 | t.datetime "created_at" 10 | t.datetime "updated_at" 11 | t.string "image_uid" 12 | t.string "image_ext" 13 | end 14 | 15 | create_table "inquiries", :force => true do |t| 16 | t.string "name" 17 | t.string "email" 18 | t.string "phone" 19 | t.text "message" 20 | t.integer "position" 21 | t.boolean "open", :default => true 22 | t.datetime "created_at" 23 | t.datetime "updated_at" 24 | t.boolean "spam", :default => false 25 | end 26 | 27 | create_table "inquiry_settings", :force => true do |t| 28 | t.string "name" 29 | t.text "value" 30 | t.boolean "destroyable" 31 | t.datetime "created_at" 32 | t.datetime "updated_at" 33 | end 34 | 35 | create_table "page_parts", :force => true do |t| 36 | t.integer "page_id" 37 | t.string "title" 38 | t.text "body" 39 | t.integer "position" 40 | t.datetime "created_at" 41 | t.datetime "updated_at" 42 | end 43 | 44 | add_index "page_parts", ["id"], :name => "index_page_parts_on_id" 45 | add_index "page_parts", ["page_id"], :name => "index_page_parts_on_page_id" 46 | 47 | create_table "pages", :force => true do |t| 48 | t.string "title" 49 | t.integer "parent_id" 50 | t.integer "position" 51 | t.string "path" 52 | t.datetime "created_at" 53 | t.datetime "updated_at" 54 | t.string "meta_keywords" 55 | t.text "meta_description" 56 | t.boolean "show_in_menu", :default => true 57 | t.string "link_url" 58 | t.string "menu_match" 59 | t.boolean "deletable", :default => true 60 | t.string "custom_title" 61 | t.string "custom_title_type", :default => "none" 62 | t.boolean "draft", :default => false 63 | t.string "browser_title" 64 | t.boolean "skip_to_first_child", :default => false 65 | t.integer "lft" 66 | t.integer "rgt" 67 | t.integer "depth" 68 | t.string "cached_slug" 69 | end 70 | 71 | add_index "pages", ["depth"], :name => "index_pages_on_depth" 72 | add_index "pages", ["id"], :name => "index_pages_on_id" 73 | add_index "pages", ["lft"], :name => "index_pages_on_lft" 74 | add_index "pages", ["parent_id"], :name => "index_pages_on_parent_id" 75 | add_index "pages", ["rgt"], :name => "index_pages_on_rgt" 76 | 77 | create_table "refinery_settings", :force => true do |t| 78 | t.string "name" 79 | t.text "value" 80 | t.boolean "destroyable", :default => true 81 | t.datetime "created_at" 82 | t.datetime "updated_at" 83 | t.string "scoping" 84 | t.boolean "restricted", :default => false 85 | t.string "callback_proc_as_string" 86 | end 87 | 88 | add_index "refinery_settings", ["name"], :name => "index_refinery_settings_on_name" 89 | 90 | create_table "resources", :force => true do |t| 91 | t.string "file_mime_type" 92 | t.string "file_name" 93 | t.integer "file_size" 94 | t.datetime "created_at" 95 | t.datetime "updated_at" 96 | t.string "file_uid" 97 | t.string "file_ext" 98 | end 99 | 100 | create_table "roles", :force => true do |t| 101 | t.string "title" 102 | end 103 | 104 | create_table "roles_users", :id => false, :force => true do |t| 105 | t.integer "user_id" 106 | t.integer "role_id" 107 | end 108 | 109 | add_index "roles_users", ["role_id", "user_id"], :name => "index_roles_users_on_role_id_and_user_id" 110 | add_index "roles_users", ["user_id", "role_id"], :name => "index_roles_users_on_user_id_and_role_id" 111 | 112 | create_table "slugs", :force => true do |t| 113 | t.string "name" 114 | t.integer "sluggable_id" 115 | t.integer "sequence", :default => 1, :null => false 116 | t.string "sluggable_type", :limit => 40 117 | t.string "scope", :limit => 40 118 | t.datetime "created_at" 119 | end 120 | 121 | add_index "slugs", ["name", "sluggable_type", "scope", "sequence"], :name => "index_slugs_on_name_and_sluggable_type_and_scope_and_sequence", :unique => true 122 | add_index "slugs", ["sluggable_id"], :name => "index_slugs_on_sluggable_id" 123 | 124 | create_table "user_plugins", :force => true do |t| 125 | t.integer "user_id" 126 | t.string "name" 127 | t.integer "position" 128 | end 129 | 130 | add_index "user_plugins", ["name"], :name => "index_user_plugins_on_title" 131 | add_index "user_plugins", ["user_id", "name"], :name => "index_unique_user_plugins", :unique => true 132 | 133 | create_table "users", :force => true do |t| 134 | t.string "login", :null => false 135 | t.string "email", :null => false 136 | t.string "crypted_password", :null => false 137 | t.string "password_salt", :null => false 138 | t.string "persistence_token" 139 | t.datetime "created_at" 140 | t.datetime "updated_at" 141 | t.string "perishable_token" 142 | end 143 | 144 | add_index "users", ["id"], :name => "index_users_on_id" 145 | 146 | end 147 | -------------------------------------------------------------------------------- /examples/applications/spree/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2010, Rails Dog LLC and other contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name Spree nor the names of its contributors may be used to 13 | endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /examples/applications/spree/lib/delegate_belongs_to.rb: -------------------------------------------------------------------------------- 1 | module DelegateBelongsTo 2 | module ClassMethods 3 | 4 | @@default_rejected_delegate_columns = ['created_at','created_on','updated_at','updated_on','lock_version','type','id','position','parent_id','lft','rgt'] 5 | mattr_accessor :default_rejected_delegate_columns 6 | 7 | def delegate_belongs_to(association, *attrs) 8 | opts = attrs.extract_options! 9 | initialize_association :belongs_to, association, opts 10 | attrs = get_association_column_names(association) if attrs.empty? 11 | attrs.concat get_association_column_names(association) if attrs.delete :defaults 12 | attrs.each do |attr| 13 | class_def attr do |*args| 14 | if args.empty? 15 | send(:delegator_for, association).send(attr) 16 | else 17 | send(:delegator_for, association).send(attr, *args) 18 | end 19 | end 20 | class_def "#{attr}=" do |val| 21 | send(:delegator_for, association).send("#{attr}=", val) 22 | end 23 | end 24 | end 25 | 26 | protected 27 | 28 | def get_association_column_names(association, without_default_rejected_delegate_columns=true) 29 | begin 30 | association_klass = reflect_on_association(association).klass 31 | methods = association_klass.column_names 32 | methods.reject!{|x|default_rejected_delegate_columns.include?(x.to_s)} if without_default_rejected_delegate_columns 33 | return methods 34 | rescue 35 | return [] 36 | end 37 | end 38 | 39 | ## 40 | # initialize_association :belongs_to, :contact 41 | def initialize_association(type, association, opts={}) 42 | raise 'Illegal or unimplemented association type.' unless [:belongs_to].include?(type.to_s.to_sym) 43 | send type, association, opts if reflect_on_association(association).nil? 44 | end 45 | 46 | private 47 | 48 | def class_def(name, method=nil, &blk) 49 | class_eval { method.nil? ? define_method(name, &blk) : define_method(name, method) } 50 | end 51 | 52 | end 53 | 54 | module InstanceMethods 55 | protected 56 | def delegator_for(association) 57 | send("#{association}=", self.class.reflect_on_association(association).klass.new) if send(association).nil? 58 | send(association) 59 | end 60 | end 61 | 62 | def self.included(receiver) 63 | receiver.extend ClassMethods 64 | receiver.send :include, InstanceMethods 65 | end 66 | end 67 | 68 | ActiveRecord::Base.send :include, DelegateBelongsTo -------------------------------------------------------------------------------- /examples/applications/spree/models/address.rb: -------------------------------------------------------------------------------- 1 | class Address < ActiveRecord::Base 2 | belongs_to :country 3 | belongs_to :state 4 | 5 | has_many :billing_checkouts, :foreign_key => "bill_address_id", :class_name => "Checkout" 6 | has_many :shipping_checkouts, :foreign_key => "ship_address_id", :class_name => "Checkout" 7 | has_many :shipments 8 | 9 | validates :firstname, :lastname, :address1, :city, :zipcode, :country, :phone, :presence => true 10 | validates :state, :presence => true, :if => Proc.new { |address| address.state_name.blank? && Spree::Config[:address_requires_state] } 11 | validates :state_name, :presence => true, :if => Proc.new { |address| address.state.blank? && Spree::Config[:address_requires_state] } 12 | validate :state_name_validate, :if => Proc.new { |address| address.state.blank? && Spree::Config[:address_requires_state] } 13 | end 14 | -------------------------------------------------------------------------------- /examples/applications/spree/models/adjustment.rb: -------------------------------------------------------------------------------- 1 | class Adjustment < ActiveRecord::Base 2 | belongs_to :order 3 | belongs_to :source, :polymorphic => true 4 | belongs_to :originator, :polymorphic => true 5 | 6 | validates :label, :presence => true 7 | validates :amount, :numericality => true 8 | end 9 | -------------------------------------------------------------------------------- /examples/applications/spree/models/app_configuration.rb: -------------------------------------------------------------------------------- 1 | class AppConfiguration < Configuration 2 | validates :name, :presence => true, :uniqueness => true 3 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/asset.rb: -------------------------------------------------------------------------------- 1 | class Asset < ActiveRecord::Base 2 | belongs_to :viewable, :polymorphic => true 3 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/billing_integration.rb: -------------------------------------------------------------------------------- 1 | class BillingIntegration < PaymentMethod 2 | validates :name, :presence => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator.rb: -------------------------------------------------------------------------------- 1 | class Calculator < ActiveRecord::Base 2 | belongs_to :calculable, :polymorphic => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/flat_percent_item_total.rb: -------------------------------------------------------------------------------- 1 | class Calculator::FlatPercentItemTotal < Calculator 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/flat_rate.rb: -------------------------------------------------------------------------------- 1 | class Calculator::FlatRate < Calculator 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/flexi_rate.rb: -------------------------------------------------------------------------------- 1 | class Calculator::FlexiRate < Calculator 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/per_item.rb: -------------------------------------------------------------------------------- 1 | class Calculator::PerItem < Calculator 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/price_bucket.rb: -------------------------------------------------------------------------------- 1 | class Calculator::PriceBucket < Calculator 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/sales_tax.rb: -------------------------------------------------------------------------------- 1 | class Calculator::SalesTax < Calculator 2 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/calculator/vat.rb: -------------------------------------------------------------------------------- 1 | class Calculator::Vat < Calculator 2 | 3 | def self.description 4 | I18n.t("vat") 5 | end 6 | 7 | def self.register 8 | super 9 | TaxRate.register_calculator(self) 10 | end 11 | 12 | # list the vat rates for the default country 13 | def self.default_rates 14 | origin = Country.find(Spree::Config[:default_country_id]) 15 | calcs = Calculator::Vat.find(:all, :include => {:calculable => :zone}).select { 16 | |vat| vat.calculable.zone.country_list.include?(origin) 17 | } 18 | calcs.collect { |calc| calc.calculable } 19 | end 20 | 21 | def self.calculate_tax(order, rates=default_rates) 22 | return 0 if rates.empty? 23 | # note: there is a bug with associations in rails 2.1 model caching so we're using this hack 24 | # (see https://rails.lighthouseapp.com/projects/8994/tickets/785-caching-models-fails-in-development) 25 | cache_hack = rates.first.respond_to?(:tax_category_id) 26 | 27 | taxable_totals = {} 28 | order.line_items.each do |line_item| 29 | next unless tax_category = line_item.variant.product.tax_category 30 | next unless rate = rates.find { | vat_rate | vat_rate.tax_category_id == tax_category.id } if cache_hack 31 | next unless rate = rates.find { | vat_rate | vat_rate.tax_category == tax_category } unless cache_hack 32 | taxable_totals[tax_category] ||= 0 33 | taxable_totals[tax_category] += line_item.price * rate.amount * line_item.quantity 34 | end 35 | 36 | return 0 if taxable_totals.empty? 37 | tax = 0 38 | taxable_totals.values.each do |total| 39 | tax += total 40 | end 41 | tax 42 | end 43 | 44 | # TODO: Refactor this method after integrating #54 to use default address 45 | def self.calculate_tax_on(product_or_variant) 46 | vat_rates = default_rates 47 | 48 | return 0 if vat_rates.nil? 49 | return 0 unless tax_category = product_or_variant.is_a?(Product) ? product_or_variant.tax_category : product_or_variant.product.tax_category 50 | return 0 unless rate = vat_rates.find { | vat_rate | vat_rate.tax_category_id == tax_category.id } 51 | 52 | (product_or_variant.is_a?(Product) ? product_or_variant.price : product_or_variant.price) * rate.amount 53 | end 54 | 55 | # computes vat for line_items associated with order, and tax rate 56 | def compute(order) 57 | rate = self.calculable 58 | line_items = order.line_items.select { |i| i.product.tax_category == rate.tax_category } 59 | line_items.inject(0) {|sum, line_item| 60 | sum += (line_item.price * rate.amount * line_item.quantity) 61 | } 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /examples/applications/spree/models/checkout.rb: -------------------------------------------------------------------------------- 1 | class Checkout < ActiveRecord::Base 2 | before_update :check_addresses_on_duplication, :if => "!ship_address.nil? && !bill_address.nil?" 3 | after_save :update_order_shipment 4 | before_validation :clone_billing_address, :if => "@use_billing" 5 | 6 | belongs_to :order 7 | belongs_to :bill_address, :foreign_key => "bill_address_id", :class_name => "Address" 8 | belongs_to :ship_address, :foreign_key => "ship_address_id", :class_name => "Address" 9 | belongs_to :shipping_method 10 | has_many :payments, :as => :payable 11 | 12 | validates :order_id, :shipping_method_id, :presence => true 13 | validates :email, :format => { :with => /\A\S+@\S+\.\S+\z/ } 14 | end 15 | -------------------------------------------------------------------------------- /examples/applications/spree/models/configuration.rb: -------------------------------------------------------------------------------- 1 | class Configuration < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/country.rb: -------------------------------------------------------------------------------- 1 | class Country < ActiveRecord::Base 2 | has_many :states 3 | has_one :zone_member, :as => :zoneable 4 | has_one :zone, :through => :zone_member 5 | 6 | validates :name, :iso_name, :presence => true 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/creditcard.rb: -------------------------------------------------------------------------------- 1 | class Creditcard < ActiveRecord::Base 2 | has_many :payments, :as => :source 3 | 4 | validates :month, :year, :numericality => { :only_integer => true } 5 | validates :number, :presence => true, :unless => :has_payment_profile?, :on => :create 6 | validates :verification_value, :presence => true, :unless => :has_payment_profile?, :on => :create 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway.rb: -------------------------------------------------------------------------------- 1 | class Gateway < PaymentMethod 2 | delegate_belongs_to :provider, :authorize, :purchase, :capture, :void, :credit 3 | validates :name, :type, :presence => true 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/authorize_net.rb: -------------------------------------------------------------------------------- 1 | class Gateway::AuthorizeNet < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/authorize_net_cim.rb: -------------------------------------------------------------------------------- 1 | class Gateway::AuthorizeNetCim < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/beanstream.rb: -------------------------------------------------------------------------------- 1 | class Gateway::Beanstream < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/bogus.rb: -------------------------------------------------------------------------------- 1 | class Gateway::Bogus < Gateway 2 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/eway.rb: -------------------------------------------------------------------------------- 1 | class Gateway::Eway < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/linkpoint.rb: -------------------------------------------------------------------------------- 1 | class Gateway::Linkpoint < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/pay_pal.rb: -------------------------------------------------------------------------------- 1 | class Gateway::PayPal < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/gateway/sage_pay.rb: -------------------------------------------------------------------------------- 1 | class Gateway::SagePay < Gateway 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/image.rb: -------------------------------------------------------------------------------- 1 | class Image < Asset 2 | validate :no_attachement_errors 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/spree/models/inventory_unit.rb: -------------------------------------------------------------------------------- 1 | class InventoryUnit < ActiveRecord::Base 2 | belongs_to :variant 3 | belongs_to :order 4 | belongs_to :shipment 5 | belongs_to :return_authorization 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/spree/models/line_item.rb: -------------------------------------------------------------------------------- 1 | class LineItem < ActiveRecord::Base 2 | belongs_to :order 3 | belongs_to :variant 4 | has_one :product, :through => :variant 5 | 6 | validates :variant, :presence => true 7 | validates :quantity, :numericality => { :only_integer => true, :message => I18n.t("validation.must_be_int") } 8 | validates :price, :numericality => true 9 | end 10 | -------------------------------------------------------------------------------- /examples/applications/spree/models/mail_method.rb: -------------------------------------------------------------------------------- 1 | class MailMethod < ActiveRecord::Base 2 | validates :environment, :presence => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/spree/models/option_type.rb: -------------------------------------------------------------------------------- 1 | class OptionType < ActiveRecord::Base 2 | if ActiveRecord::VERSION::MAJOR >= 4 3 | has_many :option_values, lambda { order(:position) }, :dependent => :destroy 4 | else 5 | has_many :option_values, :order => :position, :dependent => :destroy 6 | end 7 | has_many :product_option_types, :dependent => :destroy 8 | has_and_belongs_to_many :prototypes 9 | validates :name, :presentation, :presence => true 10 | end 11 | -------------------------------------------------------------------------------- /examples/applications/spree/models/option_value.rb: -------------------------------------------------------------------------------- 1 | class OptionValue < ActiveRecord::Base 2 | belongs_to :option_type 3 | has_and_belongs_to_many :variants 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/spree/models/order.rb: -------------------------------------------------------------------------------- 1 | class Order < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :bill_address, :foreign_key => "bill_address_id", :class_name => "Address" 4 | belongs_to :ship_address, :foreign_key => "ship_address_id", :class_name => "Address" 5 | belongs_to :shipping_method 6 | has_many :state_events, :as => :stateful 7 | has_many :line_items, :dependent => :destroy 8 | has_many :inventory_units 9 | has_many :payments, :dependent => :destroy 10 | has_many :shipments, :dependent => :destroy 11 | has_many :return_authorizations, :dependent => :destroy 12 | has_many :adjustments, :dependent => :destroy 13 | 14 | validates_presence_of :email, :if => :require_email 15 | end 16 | -------------------------------------------------------------------------------- /examples/applications/spree/models/payment.rb: -------------------------------------------------------------------------------- 1 | class Payment < ActiveRecord::Base 2 | belongs_to :order 3 | belongs_to :source, :polymorphic => true 4 | belongs_to :payment_method 5 | has_many :transactions 6 | if ActiveRecord::VERSION::MAJOR >= 4 7 | has_many :offsets, lambda { where("source_type = 'Payment' AND amount < 0") }, :class_name => 'Payment', :foreign_key => 'source_id' 8 | else 9 | has_many :offsets, :class_name => 'Payment', :foreign_key => 'source_id', :conditions => "source_type = 'Payment' AND amount < 0" 10 | end 11 | 12 | validates :payment_method, :presence => true, :if => Proc.new { |payable| payable.is_a? Checkout } 13 | end 14 | -------------------------------------------------------------------------------- /examples/applications/spree/models/payment_method.rb: -------------------------------------------------------------------------------- 1 | class PaymentMethod < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/payment_method/check.rb: -------------------------------------------------------------------------------- 1 | class PaymentMethod::Check < PaymentMethod 2 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/preference.rb: -------------------------------------------------------------------------------- 1 | class Preference < ActiveRecord::Base 2 | belongs_to :owner, :polymorphic => true 3 | belongs_to :group, :polymorphic => true 4 | 5 | validates :name, :owner_id, :owner_type, :presence => true 6 | validates :group_type, :presence => true, :if => :group_id? 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ActiveRecord::Base 2 | has_many :product_option_types, :dependent => :destroy 3 | has_many :option_types, :through => :product_option_types 4 | has_many :product_properties, :dependent => :destroy 5 | has_many :properties, :through => :product_properties 6 | has_and_belongs_to_many :product_groups 7 | belongs_to :tax_category 8 | has_and_belongs_to_many :taxons 9 | belongs_to :shipping_category 10 | if ActiveRecord::VERSION::MAJOR >= 4 11 | has_many :images, lambda { order(:position) }, :as => :viewable, :dependent => :destroy 12 | has_one :master, 13 | lambda { where(["variants.is_master = ? AND variants.deleted_at IS NULL", true]) }, 14 | :class_name => 'Variant' 15 | has_many :variants, 16 | lambda { where(["variants.is_master = ? AND variants.deleted_at IS NULL", false]) } 17 | has_many :variants_including_master, 18 | lambda { where(["variants.deleted_at IS NULL"]) }, 19 | :class_name => 'Variant', 20 | :dependent => :destroy 21 | else 22 | has_many :images, :as => :viewable, :order => :position, :dependent => :destroy 23 | has_one :master, 24 | :class_name => 'Variant', 25 | :conditions => ["variants.is_master = ? AND variants.deleted_at IS NULL", true] 26 | has_many :variants, 27 | :conditions => ["variants.is_master = ? AND variants.deleted_at IS NULL", false] 28 | has_many :variants_including_master, 29 | :class_name => 'Variant', 30 | :conditions => ["variants.deleted_at IS NULL"], 31 | :dependent => :destroy 32 | end 33 | delegate_belongs_to :master, :sku, :price, :weight, :height, :width, :depth, :is_master 34 | delegate_belongs_to :master, :cost_price if Variant.table_exists? && Variant.column_names.include?("cost_price") 35 | 36 | validates :name, :price, :permalink, :presence => true 37 | end 38 | -------------------------------------------------------------------------------- /examples/applications/spree/models/product_group.rb: -------------------------------------------------------------------------------- 1 | class ProductGroup < ActiveRecord::Base 2 | validates :name, :presence => true 3 | validates_associated :product_scopes 4 | 5 | has_and_belongs_to_many :cached_products, :class_name => "Product" 6 | has_many :product_scopes 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/product_option_type.rb: -------------------------------------------------------------------------------- 1 | class ProductOptionType < ActiveRecord::Base 2 | belongs_to :product 3 | belongs_to :option_type 4 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/product_property.rb: -------------------------------------------------------------------------------- 1 | class ProductProperty < ActiveRecord::Base 2 | belongs_to :product 3 | belongs_to :property 4 | 5 | validates :property, :presence => true 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/spree/models/product_scope.rb: -------------------------------------------------------------------------------- 1 | class ProductScope < ActiveRecord::Base 2 | belongs_to :product_group 3 | 4 | validate :check_validity_of_scope 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/spree/models/property.rb: -------------------------------------------------------------------------------- 1 | class Property < ActiveRecord::Base 2 | has_and_belongs_to_many :prototypes 3 | has_many :product_properties, :dependent => :destroy 4 | has_many :products, :through => :product_properties 5 | 6 | validates :name, :presentation, :presence => true 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/prototype.rb: -------------------------------------------------------------------------------- 1 | class Prototype < ActiveRecord::Base 2 | has_and_belongs_to_many :properties 3 | has_and_belongs_to_many :option_types 4 | validates :name, :presence => true 5 | end -------------------------------------------------------------------------------- /examples/applications/spree/models/return_authorization.rb: -------------------------------------------------------------------------------- 1 | class ReturnAuthorization < ActiveRecord::Base 2 | belongs_to :order 3 | has_many :inventory_units 4 | 5 | validates :order, :presence => true 6 | validates :amount, :numericality => true 7 | validate :must_have_shipped_units 8 | end 9 | -------------------------------------------------------------------------------- /examples/applications/spree/models/role.rb: -------------------------------------------------------------------------------- 1 | class Role < ActiveRecord::Base 2 | has_and_belongs_to_many :users 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/spree/models/shipment.rb: -------------------------------------------------------------------------------- 1 | class Shipment < ActiveRecord::Base 2 | belongs_to :order 3 | belongs_to :shipping_method 4 | belongs_to :address 5 | has_many :state_events, :as => :stateful 6 | has_many :inventory_units 7 | 8 | validates :inventory_units, :presence => true, :if => :require_inventory 9 | validate :shipping_method 10 | end 11 | -------------------------------------------------------------------------------- /examples/applications/spree/models/shipping_category.rb: -------------------------------------------------------------------------------- 1 | class ShippingCategory < ActiveRecord::Base 2 | validates :name, :presence => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/spree/models/shipping_method.rb: -------------------------------------------------------------------------------- 1 | class ShippingMethod < ActiveRecord::Base 2 | belongs_to :zone 3 | has_many :shipments 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/spree/models/state.rb: -------------------------------------------------------------------------------- 1 | class State < ActiveRecord::Base 2 | belongs_to :country 3 | has_one :zone_member, :as => :zoneable 4 | has_one :zone, :through => :zone_member 5 | 6 | validates :country, :name, :presence => true 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/state_event.rb: -------------------------------------------------------------------------------- 1 | class StateEvent < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :stateful, :polymorphic => true 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/spree/models/tax_category.rb: -------------------------------------------------------------------------------- 1 | class TaxCategory < ActiveRecord::Base 2 | has_many :tax_rates 3 | 4 | validates :name, :presence => true, :uniqueness => true 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/spree/models/tax_rate.rb: -------------------------------------------------------------------------------- 1 | class TaxRate < ActiveRecord::Base 2 | belongs_to :zone 3 | belongs_to :tax_category 4 | 5 | validates :amount, :presence => true, :numericality => true 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/spree/models/taxon.rb: -------------------------------------------------------------------------------- 1 | class Taxon < ActiveRecord::Base 2 | belongs_to :taxonomy 3 | has_and_belongs_to_many :products 4 | 5 | validates :name, :presence => true 6 | end 7 | -------------------------------------------------------------------------------- /examples/applications/spree/models/taxonomy.rb: -------------------------------------------------------------------------------- 1 | class Taxonomy < ActiveRecord::Base 2 | has_many :taxons, :dependent => :destroy 3 | if ActiveRecord::VERSION::MAJOR >= 4 4 | has_one :root, lambda { where("parent_id is null") }, :class_name => 'Taxon' 5 | else 6 | has_one :root, :class_name => 'Taxon', :conditions => "parent_id is null" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/applications/spree/models/tracker.rb: -------------------------------------------------------------------------------- 1 | class Tracker < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/spree/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :orders 3 | belongs_to :ship_address, :foreign_key => "ship_address_id", :class_name => "Address" 4 | belongs_to :bill_address, :foreign_key => "bill_address_id", :class_name => "Address" 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/spree/models/variant.rb: -------------------------------------------------------------------------------- 1 | class Variant < ActiveRecord::Base 2 | belongs_to :product 3 | delegate_belongs_to :product, :name, :description, :permalink, :available_on, :tax_category_id, :shipping_category_id, :meta_description, :meta_keywords 4 | 5 | has_many :inventory_units 6 | has_many :line_items 7 | has_and_belongs_to_many :option_values 8 | if ActiveRecord::VERSION::MAJOR >= 4 9 | has_many :images, lambda { order(:position) }, :as => :viewable, :dependent => :destroy 10 | else 11 | has_many :images, :as => :viewable, :order => :position, :dependent => :destroy 12 | end 13 | 14 | validate :check_price 15 | validates :price, :presence => true 16 | validates :cost_price, :numericality => true, :allow_nil => true if Variant.table_exists? && Variant.column_names.include?("cost_price") 17 | end 18 | -------------------------------------------------------------------------------- /examples/applications/spree/models/zone.rb: -------------------------------------------------------------------------------- 1 | class Zone < ActiveRecord::Base 2 | has_many :zone_members 3 | has_many :tax_rates 4 | has_many :shipping_methods 5 | 6 | validates :name, :presence => true, :uniqueness => true 7 | end 8 | -------------------------------------------------------------------------------- /examples/applications/spree/models/zone_member.rb: -------------------------------------------------------------------------------- 1 | class ZoneMember < ActiveRecord::Base 2 | belongs_to :zone 3 | belongs_to :zoneable, :polymorphic => true 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/spree/options.rb: -------------------------------------------------------------------------------- 1 | { :attributes => false } 2 | -------------------------------------------------------------------------------- /examples/applications/typo/MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005 Tobias Luetke 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /examples/applications/typo/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < Content 2 | if ActiveRecord::VERSION::MAJOR >= 4 3 | has_many :pings, lambda { order(:created_at => :asc) }, :dependent => :destroy 4 | has_many :comments, lambda { order(:created_at => :asc) }, :dependent => :destroy 5 | has_many :published_comments, lambda { order(:created_at => :asc) }, :class_name => "Comment" 6 | has_many :published_trackbacks, lambda { order(:created_at => :asc) }, :class_name => "Trackback" 7 | has_many :published_feedback, lambda { order(:created_at => :asc) }, :class_name => "Feedback" 8 | has_many :trackbacks, lambda { order(:created_at => :asc) }, :dependent => :destroy 9 | has_many :feedback, lambda { order(:created_at => :asc) } 10 | has_many :resources, lambda { order(:created_at => :asc) }, 11 | :class_name => "Resource", :foreign_key => 'article_id' 12 | has_many :categories, lambda { includes(:categorizations).order("categorizations.is_primary" => :desc).select("categories.*") }, 13 | :through => :categorizations 14 | else 15 | has_many :pings, :dependent => :destroy, :order => "created_at ASC" 16 | has_many :comments, :dependent => :destroy, :order => "created_at ASC" 17 | has_many :published_comments, :class_name => "Comment", :order => "created_at ASC" 18 | has_many :published_trackbacks, :class_name => "Trackback", :order => "created_at ASC" 19 | has_many :published_feedback, :class_name => "Feedback", :order => "created_at ASC" 20 | has_many :trackbacks, :dependent => :destroy, :order => "created_at ASC" 21 | has_many :feedback, :order => "created_at DESC" 22 | has_many :resources, :order => "created_at DESC", 23 | :class_name => "Resource", :foreign_key => 'article_id' 24 | has_many :categories, \ 25 | :through => :categorizations, \ 26 | :include => :categorizations, \ 27 | :select => 'categories.*', \ 28 | :uniq => true, \ 29 | :order => 'categorizations.is_primary DESC' 30 | end 31 | has_many :categorizations 32 | has_and_belongs_to_many :tags, :foreign_key => 'article_id' 33 | belongs_to :user 34 | has_many :triggers, :as => :pending_item 35 | 36 | validates_uniqueness_of :guid 37 | validates_presence_of :title 38 | end 39 | -------------------------------------------------------------------------------- /examples/applications/typo/models/blog.rb: -------------------------------------------------------------------------------- 1 | class Blog < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/typo/models/categorization.rb: -------------------------------------------------------------------------------- 1 | class Categorization < ActiveRecord::Base 2 | belongs_to :article 3 | belongs_to :category 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/typo/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ActiveRecord::Base 2 | has_many :categorizations 3 | if ActiveRecord::VERSION::MAJOR >= 4 4 | has_many :articles, lambda { order(:published_at => :desc, :created_at => :desc) }, 5 | :through => :categorizations 6 | else 7 | has_many :articles, 8 | :through => :categorizations, 9 | :order => "published_at DESC, created_at DESC" 10 | end 11 | 12 | validates_presence_of :name 13 | validates_uniqueness_of :name, :on => :create 14 | end 15 | -------------------------------------------------------------------------------- /examples/applications/typo/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < Feedback 2 | belongs_to :article 3 | belongs_to :user 4 | validates_presence_of :author, :body 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/typo/models/content.rb: -------------------------------------------------------------------------------- 1 | class Content < ActiveRecord::Base 2 | belongs_to :text_filter 3 | has_many :notifications, :foreign_key => 'content_id' 4 | if ActiveRecord::VERSION::MAJOR >= 4 5 | has_many :notify_users, :through => :notifications, 6 | :source => 'notify_user' 7 | else 8 | has_many :notify_users, :through => :notifications, 9 | :source => 'notify_user', 10 | :uniq => true 11 | end 12 | has_many :triggers, :as => :pending_item, :dependent => :delete_all 13 | end 14 | -------------------------------------------------------------------------------- /examples/applications/typo/models/feedback.rb: -------------------------------------------------------------------------------- 1 | class Feedback < Content 2 | self.table_name = "feedback" 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/typo/models/notification.rb: -------------------------------------------------------------------------------- 1 | class Notification < ActiveRecord::Base 2 | belongs_to :notify_content, :class_name => 'Content', :foreign_key => 'content_id' 3 | belongs_to :notify_user, :class_name => 'User', :foreign_key => 'user_id' 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/typo/models/page.rb: -------------------------------------------------------------------------------- 1 | class Page < Content 2 | belongs_to :user 3 | validates_presence_of :name, :title, :body 4 | validates_uniqueness_of :name 5 | end 6 | -------------------------------------------------------------------------------- /examples/applications/typo/models/ping.rb: -------------------------------------------------------------------------------- 1 | class Ping < ActiveRecord::Base 2 | belongs_to :article 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/typo/models/profile.rb: -------------------------------------------------------------------------------- 1 | class Profile < ActiveRecord::Base 2 | validates_uniqueness_of :label 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/typo/models/redirect.rb: -------------------------------------------------------------------------------- 1 | class Redirect < ActiveRecord::Base 2 | validates_uniqueness_of :from_path 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/typo/models/resource.rb: -------------------------------------------------------------------------------- 1 | class Resource < ActiveRecord::Base 2 | validates_uniqueness_of :filename 3 | belongs_to :article 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/typo/models/right.rb: -------------------------------------------------------------------------------- 1 | class Right < ActiveRecord::Base 2 | validates_uniqueness_of :name 3 | has_and_belongs_to_many :profiles 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/typo/models/sidebar.rb: -------------------------------------------------------------------------------- 1 | class Sidebar < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/typo/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ActiveRecord::Base 2 | if ActiveRecord::VERSION::MAJOR >= 4 3 | has_and_belongs_to_many :articles, lambda { order(:created_at => :desc) } 4 | else 5 | has_and_belongs_to_many :articles, :order => 'created_at DESC' 6 | end 7 | validates_uniqueness_of :name 8 | end 9 | -------------------------------------------------------------------------------- /examples/applications/typo/models/text_filter.rb: -------------------------------------------------------------------------------- 1 | class TextFilter < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/applications/typo/models/trackback.rb: -------------------------------------------------------------------------------- 1 | class Trackback < Feedback 2 | belongs_to :article 3 | validates_presence_of :title, :excerpt, :url 4 | end 5 | -------------------------------------------------------------------------------- /examples/applications/typo/models/trigger.rb: -------------------------------------------------------------------------------- 1 | class Trigger < ActiveRecord::Base 2 | belongs_to :pending_item, :polymorphic => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/applications/typo/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | belongs_to :profile 3 | belongs_to :text_filter 4 | has_many :notifications, :foreign_key => 'notify_user_id' 5 | if ActiveRecord::VERSION::MAJOR >= 4 6 | has_many :notify_contents, :through => :notifications, 7 | :source => 'notify_content' 8 | has_many :articles, lambda { order(:created_at => :desc) } 9 | has_many :published_articles, lambda { where(:published => true).order(:published_at => :desc) }, 10 | :class_name => 'Article' 11 | else 12 | has_many :notify_contents, :through => :notifications, 13 | :source => 'notify_content', 14 | :uniq => true 15 | has_many :articles, :order => 'created_at DESC' 16 | has_many :published_articles, 17 | :class_name => 'Article', 18 | :conditions => { :published => true }, 19 | :order => "published_at DESC" 20 | end 21 | 22 | validates_uniqueness_of :login, :on => :create 23 | validates_uniqueness_of :email, :on => :create 24 | validates_length_of :password, :within => 5..40, :if => Proc.new { |user| 25 | user.read_attribute('password').nil? or user.password.to_s.length > 0 26 | } 27 | validates_presence_of :login 28 | validates_presence_of :email 29 | validates_confirmation_of :password 30 | validates_length_of :login, :within => 3..40 31 | end 32 | -------------------------------------------------------------------------------- /examples/applications/typo/options.rb: -------------------------------------------------------------------------------- 1 | { :disconnected => false, :inheritance => true } 2 | -------------------------------------------------------------------------------- /examples/applications/typo/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 92) do 2 | 3 | create_table "articles_tags", :id => false, :force => true do |t| 4 | t.integer "article_id" 5 | t.integer "tag_id" 6 | end 7 | 8 | create_table "blogs", :force => true do |t| 9 | t.text "settings" 10 | t.string "base_url" 11 | end 12 | 13 | create_table "categories", :force => true do |t| 14 | t.string "name" 15 | t.integer "position" 16 | t.string "permalink" 17 | t.text "keywords" 18 | t.text "description" 19 | t.integer "parent_id" 20 | end 21 | 22 | add_index "categories", ["permalink"], :name => "index_categories_on_permalink" 23 | 24 | create_table "categorizations", :force => true do |t| 25 | t.integer "article_id" 26 | t.integer "category_id" 27 | t.boolean "is_primary" 28 | end 29 | 30 | create_table "contents", :force => true do |t| 31 | t.string "type" 32 | t.string "title" 33 | t.string "author" 34 | t.text "body" 35 | t.text "extended" 36 | t.text "excerpt" 37 | t.datetime "created_at" 38 | t.datetime "updated_at" 39 | t.integer "user_id" 40 | t.string "permalink" 41 | t.string "guid" 42 | t.integer "text_filter_id" 43 | t.text "whiteboard" 44 | t.string "name" 45 | t.boolean "published", :default => false 46 | t.boolean "allow_pings" 47 | t.boolean "allow_comments" 48 | t.datetime "published_at" 49 | t.string "state" 50 | t.integer "parent_id" 51 | t.string "password" 52 | end 53 | 54 | add_index "contents", ["published"], :name => "index_contents_on_published" 55 | add_index "contents", ["text_filter_id"], :name => "index_contents_on_text_filter_id" 56 | 57 | create_table "feedback", :force => true do |t| 58 | t.string "type" 59 | t.string "title" 60 | t.string "author" 61 | t.text "body" 62 | t.text "excerpt" 63 | t.datetime "created_at" 64 | t.datetime "updated_at" 65 | t.integer "user_id" 66 | t.string "guid" 67 | t.integer "text_filter_id" 68 | t.text "whiteboard" 69 | t.integer "article_id" 70 | t.string "email" 71 | t.string "url" 72 | t.string "ip", :limit => 40 73 | t.string "blog_name" 74 | t.boolean "published", :default => false 75 | t.datetime "published_at" 76 | t.string "state" 77 | t.boolean "status_confirmed" 78 | end 79 | 80 | add_index "feedback", ["article_id"], :name => "index_feedback_on_article_id" 81 | add_index "feedback", ["text_filter_id"], :name => "index_feedback_on_text_filter_id" 82 | 83 | create_table "notifications", :force => true do |t| 84 | t.integer "content_id" 85 | t.integer "user_id" 86 | t.datetime "created_at" 87 | t.datetime "updated_at" 88 | end 89 | 90 | create_table "pings", :force => true do |t| 91 | t.integer "article_id" 92 | t.string "url" 93 | t.datetime "created_at" 94 | end 95 | 96 | add_index "pings", ["article_id"], :name => "index_pings_on_article_id" 97 | 98 | create_table "profiles", :force => true do |t| 99 | t.string "label" 100 | t.string "nicename" 101 | t.text "modules" 102 | end 103 | 104 | create_table "profiles_rights", :id => false, :force => true do |t| 105 | t.integer "profile_id" 106 | t.integer "right_id" 107 | end 108 | 109 | create_table "redirects", :force => true do |t| 110 | t.string "from_path" 111 | t.string "to_path" 112 | end 113 | 114 | create_table "resources", :force => true do |t| 115 | t.integer "size" 116 | t.string "filename" 117 | t.string "mime" 118 | t.datetime "created_at" 119 | t.datetime "updated_at" 120 | t.integer "article_id" 121 | t.boolean "itunes_metadata" 122 | t.string "itunes_author" 123 | t.string "itunes_subtitle" 124 | t.integer "itunes_duration" 125 | t.text "itunes_summary" 126 | t.string "itunes_keywords" 127 | t.string "itunes_category" 128 | t.boolean "itunes_explicit" 129 | end 130 | 131 | create_table "rights", :force => true do |t| 132 | t.string "name" 133 | t.string "description" 134 | end 135 | 136 | create_table "sidebars", :force => true do |t| 137 | t.integer "active_position" 138 | t.text "config" 139 | t.integer "staged_position" 140 | t.string "type" 141 | end 142 | 143 | create_table "tags", :force => true do |t| 144 | t.string "name" 145 | t.datetime "created_at" 146 | t.datetime "updated_at" 147 | t.string "display_name" 148 | end 149 | 150 | create_table "text_filters", :force => true do |t| 151 | t.string "name" 152 | t.string "description" 153 | t.string "markup" 154 | t.text "filters" 155 | t.text "params" 156 | end 157 | 158 | create_table "triggers", :force => true do |t| 159 | t.integer "pending_item_id" 160 | t.string "pending_item_type" 161 | t.datetime "due_at" 162 | t.string "trigger_method" 163 | end 164 | 165 | create_table "users", :force => true do |t| 166 | t.string "login" 167 | t.string "password" 168 | t.text "email" 169 | t.text "name" 170 | t.boolean "notify_via_email" 171 | t.boolean "notify_on_new_articles" 172 | t.boolean "notify_on_comments" 173 | t.boolean "notify_watch_my_articles" 174 | t.string "jabber" 175 | t.integer "profile_id" 176 | t.string "remember_token" 177 | t.datetime "remember_token_expires_at" 178 | t.string "text_filter_id", :default => "1" 179 | t.string "editor", :default => "simple" 180 | t.string "state", :default => "active" 181 | t.string "firstname" 182 | t.string "lastname" 183 | t.string "nickname" 184 | t.string "url" 185 | t.string "msn" 186 | t.string "aim" 187 | t.string "yahoo" 188 | t.string "twitter" 189 | t.text "description" 190 | t.boolean "show_url" 191 | t.boolean "show_msn" 192 | t.boolean "show_aim" 193 | t.boolean "show_yahoo" 194 | t.boolean "show_twitter" 195 | t.boolean "show_jabber" 196 | t.datetime "last_connection" 197 | end 198 | 199 | end 200 | -------------------------------------------------------------------------------- /examples/associations/many-to-many-indirect/models/spell.rb: -------------------------------------------------------------------------------- 1 | class Spell < ActiveRecord::Base 2 | has_many :spell_masteries 3 | end 4 | -------------------------------------------------------------------------------- /examples/associations/many-to-many-indirect/models/spell_mastery.rb: -------------------------------------------------------------------------------- 1 | class SpellMastery < ActiveRecord::Base 2 | belongs_to :wizard 3 | belongs_to :spell 4 | validates_presence_of :wizard, :spell 5 | end 6 | -------------------------------------------------------------------------------- /examples/associations/many-to-many-indirect/models/wizard.rb: -------------------------------------------------------------------------------- 1 | class Wizard < ActiveRecord::Base 2 | has_many :spell_masteries 3 | has_many :spells, :through => :spell_masteries 4 | validates_presence_of :spells, :spell_masteries 5 | end 6 | -------------------------------------------------------------------------------- /examples/associations/many-to-many-indirect/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false, :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/associations/many-to-many-indirect/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "wizards", :force => true do |t| 3 | t.string :name, :null => false 4 | t.date :graduated_on 5 | end 6 | 7 | create_table "spells", :force => true do |t| 8 | t.string :formula, :null => false 9 | t.string :nickname 10 | end 11 | 12 | create_table "spell_masteries", :force => true do |t| 13 | t.references :wizard, :null => false 14 | t.references :spell, :null => false 15 | t.integer :strength 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/associations/many-to-many/models/film.rb: -------------------------------------------------------------------------------- 1 | class Film < ActiveRecord::Base 2 | has_and_belongs_to_many :genres 3 | end 4 | -------------------------------------------------------------------------------- /examples/associations/many-to-many/models/genre.rb: -------------------------------------------------------------------------------- 1 | class Genre < ActiveRecord::Base 2 | has_and_belongs_to_many :films 3 | end 4 | -------------------------------------------------------------------------------- /examples/associations/many-to-many/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false, :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/associations/many-to-many/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "genres", :force => true do |t| 3 | t.string :name, :null => false 4 | t.string :description 5 | end 6 | 7 | create_table "films", :force => true do |t| 8 | t.string :title, :null => false 9 | t.date :release_date 10 | t.float :rating 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/associations/one-to-many/models/cannon.rb: -------------------------------------------------------------------------------- 1 | class Cannon < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/associations/one-to-many/models/galleon.rb: -------------------------------------------------------------------------------- 1 | class Galleon < ActiveRecord::Base 2 | has_many :cannons 3 | validates_length_of :cannons, :maximum => 36 4 | end 5 | -------------------------------------------------------------------------------- /examples/associations/one-to-many/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false, :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/associations/one-to-many/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "galleons", :force => true do |t| 3 | t.string :name, :null => false 4 | t.integer :mast_count, :null => false 5 | t.date :completed_on 6 | end 7 | 8 | create_table "cannons", :force => true do |t| 9 | t.references :galleon, :null => false 10 | t.integer :calibre 11 | t.integer :barrel_length 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/associations/one-to-one-recursive/models/emperor.rb: -------------------------------------------------------------------------------- 1 | class Emperor < ActiveRecord::Base 2 | belongs_to :predecessor, :class_name => "Emperor" 3 | has_one :successor, :class_name => "Emperor", :foreign_key => :predecessor_id 4 | end 5 | -------------------------------------------------------------------------------- /examples/associations/one-to-one-recursive/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false } 2 | -------------------------------------------------------------------------------- /examples/associations/one-to-one-recursive/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "emperors", :force => true do |t| 3 | t.string :name, :null => false 4 | t.boolean :murdered 5 | t.references :predecessor 6 | t.date :reigned_from 7 | t.date :reigned_until 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /examples/associations/one-to-one/models/country.rb: -------------------------------------------------------------------------------- 1 | class Country < ActiveRecord::Base 2 | has_one :head_of_state 3 | end 4 | -------------------------------------------------------------------------------- /examples/associations/one-to-one/models/head_of_state.rb: -------------------------------------------------------------------------------- 1 | class HeadOfState < ActiveRecord::Base 2 | self.table_name = :heads_of_state 3 | belongs_to :country 4 | validates_presence_of :country 5 | end 6 | -------------------------------------------------------------------------------- /examples/associations/one-to-one/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false, :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/associations/one-to-one/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "countries", :force => true do |t| 3 | t.string :official_name, :null => false 4 | t.string :common_name 5 | t.integer :inhabitants_count 6 | end 7 | 8 | create_table "heads_of_state", :force => true do |t| 9 | t.references :country, :null => false 10 | t.string :name, :null => false 11 | t.string :title 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/domains/orchard-company-orchard/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | has_many :orchards 3 | 4 | validates_presence_of :orchards 5 | end 6 | -------------------------------------------------------------------------------- /examples/domains/orchard-company-orchard/models/orchard.rb: -------------------------------------------------------------------------------- 1 | class Orchard < ActiveRecord::Base 2 | belongs_to :company 3 | end 4 | -------------------------------------------------------------------------------- /examples/domains/orchard-company-orchard/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false } 2 | -------------------------------------------------------------------------------- /examples/domains/orchard-company-orchard/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "companies", :force => true do |t| 3 | end 4 | 5 | create_table "orchards", :force => true do |t| 6 | t.references :company 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/domains/orchard-orchard-stand/models/orchard.rb: -------------------------------------------------------------------------------- 1 | class Orchard < ActiveRecord::Base 2 | has_one :stand 3 | end 4 | -------------------------------------------------------------------------------- /examples/domains/orchard-orchard-stand/models/stand.rb: -------------------------------------------------------------------------------- 1 | class Stand < ActiveRecord::Base 2 | belongs_to :orchard 3 | 4 | validates_presence_of :orchard 5 | end 6 | -------------------------------------------------------------------------------- /examples/domains/orchard-orchard-stand/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false } 2 | -------------------------------------------------------------------------------- /examples/domains/orchard-orchard-stand/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "orchards", :force => true do |t| 3 | end 4 | 5 | create_table "stands", :force => true do |t| 6 | t.references :orchard 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/domains/orchard/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | has_many :orchards 3 | 4 | validates_presence_of :orchards 5 | end 6 | -------------------------------------------------------------------------------- /examples/domains/orchard/models/orchard.rb: -------------------------------------------------------------------------------- 1 | class Orchard < ActiveRecord::Base 2 | belongs_to :company 3 | has_many :trees 4 | if ActiveRecord::VERSION::MAJOR >= 4 5 | has_many :recent_trees, lambda { order(:planted_on => :desc) }, :class_name => "Tree" 6 | else 7 | has_many :recent_trees, :class_name => "Tree", :order => "planted_on DESC" 8 | end 9 | has_and_belongs_to_many :picking_robots 10 | has_one :stand 11 | 12 | validates_presence_of :trees 13 | end 14 | -------------------------------------------------------------------------------- /examples/domains/orchard/models/picking_robot.rb: -------------------------------------------------------------------------------- 1 | class PickingRobot < ActiveRecord::Base 2 | has_and_belongs_to_many :orchards 3 | 4 | validates_presence_of :orchards 5 | end 6 | -------------------------------------------------------------------------------- /examples/domains/orchard/models/species.rb: -------------------------------------------------------------------------------- 1 | class Species < ActiveRecord::Base 2 | has_many :trees 3 | end 4 | -------------------------------------------------------------------------------- /examples/domains/orchard/models/stand.rb: -------------------------------------------------------------------------------- 1 | class Stand < ActiveRecord::Base 2 | belongs_to :orchard 3 | 4 | validates_presence_of :orchard 5 | end 6 | -------------------------------------------------------------------------------- /examples/domains/orchard/models/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < ActiveRecord::Base 2 | belongs_to :orchard 3 | belongs_to :species 4 | 5 | validates_presence_of :orchard 6 | validates_presence_of :species 7 | end 8 | -------------------------------------------------------------------------------- /examples/domains/orchard/options.rb: -------------------------------------------------------------------------------- 1 | { :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/domains/orchard/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "companies", :force => true do |t| 3 | t.string :name, :null => false 4 | t.date :founded_on 5 | end 6 | 7 | create_table "orchards", :force => true do |t| 8 | t.references :company 9 | t.float :acres 10 | t.string :name, :null => false 11 | t.string :location 12 | t.date :planted_on, :null => false 13 | t.integer :revenue 14 | end 15 | 16 | create_table "picking_robots", :force => true do |t| 17 | t.references :orchard 18 | t.date :last_serviced 19 | t.string :model 20 | end 21 | 22 | create_table "orchards_picking_robots", :force => true, :id => false do |t| 23 | t.references :orchard 24 | t.references :picking_robot 25 | end 26 | 27 | create_table "species", :force => true do |t| 28 | t.string :scientific_name, :null => false 29 | t.string :common_name 30 | end 31 | 32 | create_table "stands", :force => true do |t| 33 | t.references :orchard 34 | t.string :address, :null => false 35 | end 36 | 37 | create_table "trees", :force => true do |t| 38 | t.references :orchard 39 | t.references :species 40 | t.integer :grid_number, :null => false 41 | t.integer :health_rating 42 | t.integer :produce_rating 43 | t.date :planted_on 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /examples/entities/attributes/models/photograph.rb: -------------------------------------------------------------------------------- 1 | class Photograph < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/entities/attributes/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false } 2 | -------------------------------------------------------------------------------- /examples/entities/attributes/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "photographs" do |t| 3 | t.decimal :aperture 4 | t.binary :data, :null => false 5 | t.text :description, :limit => 512 6 | t.string :filename, :null => false, :limit => 64 7 | t.boolean :flash 8 | t.integer :iso 9 | t.float :shutter_speed 10 | t.datetime :taken_at, :null => false 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/erdconfig.another_example: -------------------------------------------------------------------------------- 1 | attributes: 2 | - false 3 | 4 | -------------------------------------------------------------------------------- /examples/erdconfig.example: -------------------------------------------------------------------------------- 1 | attributes: 2 | - content 3 | - foreign_key 4 | - inheritance 5 | - false 6 | disconnected: true 7 | filename: erd 8 | filetype: pdf 9 | indirect: true 10 | inheritance: false 11 | markup: true 12 | notation: simple 13 | orientation: horizontal 14 | polymorphism: false 15 | sort: true 16 | warn: true 17 | title: sample title 18 | exclude: null 19 | only: null 20 | only_recursion_depth: null 21 | prepend_primary: false 22 | cluster: false 23 | fonts: 24 | normal: "Arial" 25 | bold: "Arial Bold" 26 | italic: "Arial Italic" 27 | -------------------------------------------------------------------------------- /examples/generate.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | 4 | require "active_record" 5 | require "rails_erd/diagram/graphviz" 6 | require "rails_erd/diagram/mermaid" 7 | require "active_support/dependencies" 8 | 9 | output_dir = File.expand_path("output", ".") 10 | FileUtils.mkdir_p output_dir 11 | Dir["#{File.dirname(__FILE__)}/*/*"].each do |path| 12 | name = File.basename(path) 13 | print "=> Generating domain for #{name.camelize}... " 14 | begin 15 | # Load database schema. 16 | ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:" 17 | ActiveRecord::Migration.suppress_messages do 18 | begin 19 | require File.expand_path("#{path}/schema.rb", File.dirname(__FILE__)) 20 | rescue LoadError 21 | end 22 | end 23 | 24 | # Load domain models for this example. 25 | ActiveSupport::Dependencies.autoload_paths = ["#{path}/models"] 26 | Dir["#{path}/{lib,models}/**/*.rb"].each do |model| 27 | require File.expand_path(model, File.dirname(__FILE__)) 28 | end 29 | 30 | # Skip empty domain models. 31 | next if ActiveRecord::Base.descendants.empty? 32 | 33 | puts "#{ActiveRecord::Base.descendants.length} models" 34 | domain = RailsERD::Domain.generate 35 | 36 | [:simple, :bachman].each do |notation| 37 | [:dot, :pdf].each do |filetype| 38 | filename = File.expand_path("#{output_dir}/#{name}#{notation != :simple ? "-#{notation}" : ""}", File.dirname(__FILE__)) 39 | 40 | default_options = { :notation => notation, :filetype => filetype, :filename => filename, 41 | :title => name.camelize + " domain model" } 42 | 43 | specific_options = eval((File.read("#{path}/options.rb") rescue "")) || {} 44 | 45 | # Generate ERD. 46 | outfile = RailsERD::Diagram::Graphviz.new(domain, default_options.merge(specific_options)).create 47 | 48 | puts " - Graphviz #{notation} notation saved to #{outfile}" 49 | end 50 | end 51 | 52 | filename = File.expand_path("#{output_dir}/#{name}", File.dirname(__FILE__)) 53 | default_options = { :filetype => "txt", :filename => filename } 54 | specific_options = eval((File.read("#{path}/options.rb") rescue "")) || {} 55 | 56 | outfile = RailsERD::Diagram::Mermaid.new(domain, default_options.merge(specific_options)).create 57 | puts " - Mermaid saved to #{outfile}" 58 | 59 | puts 60 | ensure 61 | # Completely remove all loaded Active Record models. 62 | ActiveRecord::Base.descendants.each do |model| 63 | Object.send :remove_const, model.name.to_sym rescue nil 64 | end 65 | 66 | if ActiveRecord.version >= Gem::Version.new("7.0.0") 67 | ActiveRecord::Base.subclasses.clear 68 | else 69 | ActiveRecord::Base.direct_descendants.clear 70 | end 71 | 72 | if Arel.const_defined?(:Relation) 73 | Arel::Relation.send :class_variable_set, :@@connection_tables_primary_keys, {} 74 | end 75 | ActiveSupport::Dependencies::Reference.clear! 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /examples/identity.rb: -------------------------------------------------------------------------------- 1 | # This is an experiment at using Rails ERD reflection to reconstruct the 2 | # domain model in Active Record itself. It does not support specializations or 3 | # indirect relationships. 4 | require "rails_erd/diagram" 5 | 6 | class Identity < RailsERD::Diagram 7 | setup do 8 | @class_defintions = {} 9 | end 10 | 11 | each_entity do |entity, attributes| 12 | @class_defintions[entity.name] = [] 13 | end 14 | 15 | each_relationship do |relationship| 16 | unless relationship.indirect? 17 | @class_defintions[relationship.source.name] << association_macro(relationship) 18 | @class_defintions[relationship.destination.name] << reverse_association_macro(relationship) 19 | end 20 | end 21 | 22 | save do 23 | @class_defintions.each do |klass, lines| 24 | puts "class #{klass} < ActiveRecord::Base" 25 | lines.each do |line| 26 | puts " #{line}" 27 | end 28 | puts "end" 29 | end 30 | end 31 | 32 | private 33 | 34 | def association_macro(relationship) 35 | name = relationship.destination.name.underscore 36 | case 37 | when relationship.to_one? then "has_one :#{name}" 38 | when relationship.many_to_many? then "has_and_belongs_to_many :#{name.pluralize}" 39 | when relationship.to_many? then "has_many :#{name.pluralize}" 40 | end 41 | end 42 | 43 | def reverse_association_macro(relationship) 44 | name = relationship.source.name.underscore 45 | case 46 | when relationship.many_to? then "has_and_belongs_to_many :#{name.pluralize}" 47 | when relationship.one_to? then "belongs_to :#{name}" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /examples/inheritance/single-inheritance/models/beer.rb: -------------------------------------------------------------------------------- 1 | class Beer < Beverage 2 | end 3 | -------------------------------------------------------------------------------- /examples/inheritance/single-inheritance/models/beverage.rb: -------------------------------------------------------------------------------- 1 | class Beverage < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/inheritance/single-inheritance/models/whisky.rb: -------------------------------------------------------------------------------- 1 | class Whisky < Beverage 2 | end 3 | -------------------------------------------------------------------------------- /examples/inheritance/single-inheritance/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false, :inheritance => true, :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/inheritance/single-inheritance/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "beverages", :force => true do |t| 3 | t.string :name, :null => false 4 | t.string :type 5 | t.string :brand 6 | t.integer :abv 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/models/cardinality.rb: -------------------------------------------------------------------------------- 1 | class Cardinality < ActiveRecord::Base 2 | belongs_to :relationship 3 | end 4 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/models/domain.rb: -------------------------------------------------------------------------------- 1 | class Domain < ActiveRecord::Base 2 | has_many :entities 3 | has_many :relationships 4 | has_many :specializations 5 | end 6 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/models/entity.rb: -------------------------------------------------------------------------------- 1 | class Entity < ActiveRecord::Base 2 | belongs_to :domain 3 | has_many :properties 4 | has_many :outgoing_relationships, :class_name => "Relationship", :foreign_key => :source_entity_id 5 | has_many :incoming_relationships, :class_name => "Relationship", :foreign_key => :destination_entity_id 6 | validates_presence_of :properties 7 | end 8 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/models/property.rb: -------------------------------------------------------------------------------- 1 | class Property < ActiveRecord::Base 2 | belongs_to :entity 3 | end 4 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/models/relationship.rb: -------------------------------------------------------------------------------- 1 | class Relationship < ActiveRecord::Base 2 | belongs_to :domain 3 | belongs_to :source_entity, :class_name => "Entity" 4 | belongs_to :destination_entity, :class_name => "Entity" 5 | has_one :cardinality 6 | end 7 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/models/specialization.rb: -------------------------------------------------------------------------------- 1 | class Specialization < ActiveRecord::Base 2 | belongs_to :domain 3 | belongs_to :generalized_entity, :class_name => "Entity" 4 | belongs_to :specialized_entity, :class_name => "Entity" 5 | end 6 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/options.rb: -------------------------------------------------------------------------------- 1 | { :title => "Rails ERD domain model", :orientation => :vertical } 2 | -------------------------------------------------------------------------------- /examples/meta/rails-erd/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "domains", :force => true do |t| 3 | t.string :name 4 | end 5 | 6 | create_table "entities", :force => true do |t| 7 | t.references :domain, :null => false 8 | t.string :name, :null => false 9 | t.boolean :specialized 10 | end 11 | 12 | create_table "relationships", :force => true do |t| 13 | t.references :source_entity, :null => false 14 | t.references :destination_entity, :null => false 15 | t.integer :strength, :null => false 16 | t.boolean :indirect 17 | t.boolean :mutual 18 | end 19 | 20 | create_table "specializations", :force => true do |t| 21 | t.references :generalized_entity, :null => false 22 | t.references :specialized_entity, :null => false 23 | end 24 | 25 | create_table "properties", :force => true do |t| 26 | t.references :entity, :null => false 27 | t.string :name, :null => false 28 | t.string :type, :null => false 29 | t.boolean :mandatory, :null => false 30 | end 31 | 32 | create_table "cardinalities", :force => true do |t| 33 | t.references :relationship, :null => false 34 | t.integer :source_minimum, :null => false 35 | t.integer :source_maximum 36 | t.integer :destination_minimum, :null => false 37 | t.integer :destination_maximum 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /examples/polymorphism/polymorphic-belongs-to/models/barricade.rb: -------------------------------------------------------------------------------- 1 | class Barricade < ActiveRecord::Base 2 | has_many :soldiers, :as => :defensible 3 | end 4 | -------------------------------------------------------------------------------- /examples/polymorphism/polymorphic-belongs-to/models/soldier.rb: -------------------------------------------------------------------------------- 1 | class Soldier < ActiveRecord::Base 2 | belongs_to :defensible, :polymorphic => true 3 | end 4 | -------------------------------------------------------------------------------- /examples/polymorphism/polymorphic-belongs-to/models/stronghold.rb: -------------------------------------------------------------------------------- 1 | class Stronghold < ActiveRecord::Base 2 | has_many :soldiers, :as => :defensible 3 | end 4 | -------------------------------------------------------------------------------- /examples/polymorphism/polymorphic-belongs-to/options.rb: -------------------------------------------------------------------------------- 1 | { :title => false, :polymorphism => true } 2 | -------------------------------------------------------------------------------- /examples/polymorphism/polymorphic-belongs-to/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "barricades", :force => true do |t| 3 | t.string :name, :null => false 4 | t.string :location 5 | t.boolean :upheld, :null => false 6 | end 7 | 8 | create_table "strongholds", :force => true do |t| 9 | t.string :name, :null => false 10 | t.string :location 11 | t.date :completed_on 12 | end 13 | 14 | create_table "soldiers", :force => true do |t| 15 | t.references :defensible, :null => false 16 | t.integer :health_rating, :null => false 17 | t.integer :armor_rating 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/sfdp.rb: -------------------------------------------------------------------------------- 1 | Dir["output/*.dot"].each do |file| 2 | `sfdp -Tpdf -Gmclimit=5 -GK=1 -Grepulsiveforce=40 -Gsplines=true -Goverlap=false -ooutput/#{File.basename(file, ".dot")}-sfdp.pdf #{file}` 3 | end 4 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.4.2.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 4.2.0" 6 | 7 | group :development do 8 | gem 'minitest', '5.10.1' 9 | gem 'mocha' 10 | gem "rake" 11 | gem "yard" 12 | 13 | platforms :ruby do 14 | gem "sqlite3", '~> 1.3.13' 15 | gem "redcarpet" 16 | end 17 | 18 | platforms :jruby do 19 | gem "activerecord-jdbcsqlite3-adapter" 20 | gem "jruby-openssl", :require => false # Silence openssl warnings. 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.5.0.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 5.0.2" 6 | 7 | group :development do 8 | gem 'minitest', '5.10.1' 9 | gem 'mocha' 10 | gem "rake" 11 | gem "yard" 12 | 13 | platforms :ruby do 14 | gem "sqlite3", '~> 1.3.13' 15 | gem "redcarpet" 16 | end 17 | 18 | platforms :jruby do 19 | gem "activerecord-jdbcsqlite3-adapter" 20 | gem "jruby-openssl", :require => false # Silence openssl warnings. 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.5.1.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 5.1.0" 6 | 7 | group :development do 8 | gem 'mocha' 9 | gem "rake" 10 | gem "yard" 11 | 12 | platforms :ruby do 13 | gem "sqlite3", '~> 1.3.13' 14 | gem "redcarpet" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbcsqlite3-adapter" 19 | gem "jruby-openssl", :require => false # Silence openssl warnings. 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.5.2.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 5.2.0" 6 | 7 | group :development do 8 | gem 'mocha' 9 | gem "rake" 10 | gem "yard" 11 | 12 | platforms :ruby do 13 | gem "sqlite3", '~> 1.3.13' 14 | gem "redcarpet" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbcsqlite3-adapter" 19 | gem "jruby-openssl", :require => false # Silence openssl warnings. 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.6.0.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 6.0.0rc1" 6 | 7 | group :development do 8 | gem 'mocha' 9 | gem "rake" 10 | gem "yard" 11 | 12 | platforms :ruby do 13 | gem "sqlite3", '~> 1.4' 14 | gem "redcarpet" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbcsqlite3-adapter" 19 | gem "jruby-openssl", :require => false # Silence openssl warnings. 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.6.1.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 6.1.1" 6 | 7 | group :development do 8 | gem 'mocha' 9 | gem "rake" 10 | gem "yard" 11 | 12 | platforms :ruby do 13 | gem "sqlite3", '~> 1.4' 14 | gem "redcarpet" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbcsqlite3-adapter" 19 | gem "jruby-openssl", :require => false # Silence openssl warnings. 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.7.0.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 7.0.0" 6 | 7 | group :development do 8 | gem 'mocha' 9 | gem "rake" 10 | gem "yard" 11 | 12 | platforms :ruby do 13 | gem "sqlite3", '~> 1.4' 14 | gem "redcarpet" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbcsqlite3-adapter" 19 | gem "jruby-openssl", :require => false # Silence openssl warnings. 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.edge: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", :git => "https://github.com/rails/rails", :branch => "main" 6 | 7 | group :development do 8 | gem 'mocha' 9 | gem "rake" 10 | gem "yard" 11 | 12 | platforms :ruby do 13 | gem "sqlite3" 14 | gem "redcarpet" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbcsqlite3-adapter" 19 | gem "jruby-openssl", :require => false # Silence openssl warnings. 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/erd/USAGE: -------------------------------------------------------------------------------- 1 | Add a .rake file that automatically generate the graphical models when you do 2 | a db:migrate in development mode: 3 | 4 | rails generate erd:install 5 | -------------------------------------------------------------------------------- /lib/generators/erd/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Erd 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | desc "Copy rails-erd rakefiles for automatic graphic generation" 5 | source_root File.expand_path('../templates', __FILE__) 6 | 7 | # copy rake tasks 8 | def copy_tasks 9 | template "auto_generate_diagram.rake", "lib/tasks/auto_generate_diagram.rake" 10 | end 11 | 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/erd/templates/auto_generate_diagram.rake: -------------------------------------------------------------------------------- 1 | # NOTE: only doing this in development as some production environments (Heroku) 2 | # NOTE: are sensitive to local FS writes, and besides -- it's just not proper 3 | # NOTE: to have a dev-mode tool do its thing in production. 4 | if Rails.env.development? 5 | RailsERD.load_tasks 6 | end 7 | -------------------------------------------------------------------------------- /lib/rails-erd.rb: -------------------------------------------------------------------------------- 1 | require "rails_erd" 2 | -------------------------------------------------------------------------------- /lib/rails_erd.rb: -------------------------------------------------------------------------------- 1 | require "active_support/ordered_options" 2 | require "rails_erd/railtie" if defined? Rails 3 | require "rails_erd/config" 4 | 5 | # Welcome to the API documentation of Rails ERD. If you wish to extend or 6 | # customise the output that is generated by Rails ERD, you have come to the 7 | # right place. 8 | # 9 | # == Creating custom output 10 | # 11 | # If you want to create your own kind of diagrams, or some other output, a 12 | # good starting point is the RailsERD::Diagram class. It can serve as the base 13 | # of your output generation code. 14 | # 15 | # == Options 16 | # 17 | # Rails ERD provides several options that allow you to customise the 18 | # generation of the diagram and the domain model itself. For an overview of 19 | # all options available in Rails ERD, see README.rdoc. 20 | # 21 | # You can specify the option on the command line if you use Rails ERD with 22 | # Rake: 23 | # 24 | # % rake erd orientation=vertical title='My model diagram' 25 | # 26 | # When using Rails ERD from within Ruby, you can set the options on the 27 | # RailsERD namespace module: 28 | # 29 | # RailsERD.options.orientation = :vertical 30 | # RailsERD.options.title = "My model diagram" 31 | module RailsERD 32 | class << self 33 | # Access to default options. Any instance of RailsERD::Domain and 34 | # RailsERD::Diagram will use these options unless overridden. 35 | attr_accessor :options 36 | 37 | def default_options 38 | ActiveSupport::OrderedOptions[ 39 | :generator, :graphviz, 40 | :attributes, :content, 41 | :disconnected, true, 42 | :filename, "erd", 43 | :filetype, :pdf, 44 | :fonts, {}, 45 | :indirect, true, 46 | :inheritance, false, 47 | :markup, true, 48 | :notation, :simple, 49 | :orientation, :horizontal, 50 | :polymorphism, false, 51 | :sort, true, 52 | :warn, true, 53 | :title, true, 54 | :exclude, nil, 55 | :only, nil, 56 | :only_recursion_depth, nil, 57 | :prepend_primary, false, 58 | :cluster, false, 59 | ] 60 | end 61 | 62 | def loaded_tasks=(val); @loaded_tasks = val; end 63 | def loaded_tasks; return @loaded_tasks; end 64 | 65 | def load_tasks 66 | return if(self.loaded_tasks) 67 | self.loaded_tasks = true 68 | 69 | Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each { |rake| load rake } 70 | end 71 | end 72 | 73 | module Inspectable # @private :nodoc: 74 | def inspection_attributes(*attributes) 75 | attribute_inspection = attributes.collect { |attribute| 76 | " @#{attribute}=\#{[Symbol, String].include?(#{attribute}.class) ? #{attribute}.inspect : #{attribute}}" 77 | }.join 78 | class_eval <<-RUBY 79 | def inspect 80 | "#<\#{self.class}:0x%.14x#{attribute_inspection}>" % (object_id << 1) 81 | end 82 | RUBY 83 | end 84 | end 85 | 86 | self.options = default_options.merge(Config.load) 87 | end 88 | -------------------------------------------------------------------------------- /lib/rails_erd/cli.rb: -------------------------------------------------------------------------------- 1 | require "rails_erd" 2 | require "choice" 3 | 4 | Choice.options do 5 | separator "" 6 | separator "Diagram options:" 7 | 8 | option :generator do 9 | long "--generator=Generator" 10 | desc "Generator to use (graphviz or mermaid). Defaults to graphviz." 11 | end 12 | 13 | option :title do 14 | long "--title=TITLE" 15 | desc "Replace default diagram title with a custom one." 16 | end 17 | 18 | option :notation do 19 | long "--notation=STYLE" 20 | desc "Diagram notation style, one of simple, bachman, uml or crowsfoot (avaiable only with Graphviz engine)." 21 | end 22 | 23 | option :attributes do 24 | long "--attributes=TYPE,..." 25 | desc "Attribute groups to display: false, content, primary_keys, foreign_keys, timestamps and/or inheritance." 26 | end 27 | 28 | option :orientation do 29 | long "--orientation=ORIENTATION" 30 | desc "Orientation of diagram, either horizontal (default) or vertical." 31 | end 32 | 33 | option :inheritance do 34 | long "--inheritance" 35 | desc "Display (single table) inheritance relationships." 36 | end 37 | 38 | option :polymorphism do 39 | long "--polymorphism" 40 | desc "Display polymorphic and abstract entities." 41 | end 42 | 43 | option :no_indirect do 44 | long "--direct" 45 | desc "Omit indirect relationships (through other entities)." 46 | end 47 | 48 | option :no_disconnected do 49 | long "--connected" 50 | desc "Omit entities without relationships." 51 | end 52 | 53 | option :only do 54 | long "--only" 55 | desc "Filter to only include listed models in diagram." 56 | end 57 | 58 | option :only_recursion_depth do 59 | long "--only_recursion_depth=INTEGER" 60 | desc "Recurses into relations specified by --only upto a depth N." 61 | end 62 | 63 | option :exclude do 64 | long "--exclude" 65 | desc "Filter to exclude listed models in diagram." 66 | end 67 | 68 | option :sort do 69 | long "--sort=BOOLEAN" 70 | desc "Sort attribute list alphabetically" 71 | end 72 | 73 | option :prepend_primary do 74 | long "--prepend_primary=BOOLEAN" 75 | desc "Ensure primary key is at start of attribute list" 76 | end 77 | 78 | option :cluster do 79 | long "--cluster" 80 | desc "Display models in subgraphs based on their namespace." 81 | end 82 | 83 | option :splines do 84 | long "--splines=SPLINE_TYPE" 85 | desc "Control how edges are represented. See http://www.graphviz.org/doc/info/attrs.html#d:splines for values." 86 | end 87 | 88 | separator "" 89 | separator "Output options:" 90 | 91 | option :filename do 92 | long "--filename=FILENAME" 93 | desc "Basename of the output diagram." 94 | end 95 | 96 | option :filetype do 97 | long "--filetype=TYPE" 98 | desc "Output file type. Available types depend on the diagram renderer." 99 | end 100 | 101 | option :no_markup do 102 | long "--no-markup" 103 | desc "Disable markup for enhanced compatibility of .dot output with other applications." 104 | end 105 | 106 | option :open do 107 | long "--open" 108 | desc "Open the output file after it has been saved." 109 | end 110 | 111 | separator "" 112 | separator "Common options:" 113 | 114 | option :help do 115 | long "--help" 116 | desc "Display this help message." 117 | end 118 | 119 | option :debug do 120 | long "--debug" 121 | desc "Show stack traces when an error occurs." 122 | end 123 | 124 | option :version do 125 | short "-v" 126 | long "--version" 127 | desc "Show version and quit." 128 | action do 129 | require "rails_erd/version" 130 | $stderr.puts RailsERD::BANNER 131 | exit 132 | end 133 | end 134 | 135 | option :config_file do 136 | short "-c" 137 | long "--config=FILENAME" 138 | desc "Configuration file to use" 139 | end 140 | end 141 | 142 | module RailsERD 143 | class CLI 144 | attr_reader :path, :options 145 | 146 | class << self 147 | def start 148 | path = Choice.rest.first || Dir.pwd 149 | options = Choice.choices.each_with_object({}) do |(key, value), opts| 150 | if key.start_with? "no_" 151 | opts[key.gsub("no_", "").to_sym] = !value 152 | elsif value.to_s.include? "," 153 | opts[key.to_sym] = value.split(",").map(&:to_s) 154 | else 155 | opts[key.to_sym] = value 156 | end 157 | end 158 | if options[:config_file] && options[:config_file] != '' 159 | RailsERD.options = RailsERD.default_options.merge(Config.load(options[:config_file])) 160 | end 161 | new(path, options).start 162 | end 163 | end 164 | 165 | def initialize(path, options) 166 | @path, @options = path, options 167 | require "rails_erd/diagram/graphviz" if options.generator == :graphviz 168 | end 169 | 170 | def start 171 | load_application 172 | create_diagram 173 | rescue Exception => e 174 | $stderr.puts "Failed: #{e.class}: #{e.message}" 175 | $stderr.puts e.backtrace.map { |t| " from #{t}" } if options[:debug] 176 | end 177 | 178 | private 179 | 180 | def load_application 181 | $stderr.puts "Loading application in '#{File.basename(path)}'..." 182 | environment_path = "#{path}/config/environment.rb" 183 | require environment_path 184 | 185 | if defined? Rails 186 | Rails.application.eager_load! 187 | Rails.application.config.eager_load_namespaces.each(&:eager_load!) if Rails.application.config.respond_to?(:eager_load_namespaces) 188 | end 189 | rescue ::LoadError 190 | error_message = <<~EOS 191 | Tried to load your application environment from '#{environment_path}' but the file was not present. 192 | This means that your models might not get loaded fully when the diagram gets built. This can 193 | make your entity diagram incomplete. 194 | 195 | However, if you are using ActiveRecord without Rails just make sure your models get 196 | loaded eagerly before we generate the ERD (for example, explicitly require your application 197 | bootstrap file before calling rails-erd from your Rakefile). We will continue without loading the environment file, 198 | and recommend you check your diagram for missing models after it gets generated. 199 | EOS 200 | puts error_message 201 | rescue TypeError 202 | end 203 | 204 | def generator 205 | if options.generator == :mermaid 206 | RailsERD::Diagram::Mermaid 207 | else 208 | RailsERD::Diagram::Graphviz 209 | end 210 | end 211 | 212 | def create_diagram 213 | $stderr.puts "Generating entity-relationship diagram for #{ActiveRecord::Base.descendants.length} models..." 214 | file = generator.create(options) 215 | $stderr.puts "Diagram saved to '#{file}'." 216 | `open #{file}` if options[:open] 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/rails_erd/config.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | module RailsERD 4 | class Config 5 | USER_WIDE_CONFIG_FILE = File.expand_path(".erdconfig", ENV["HOME"]) 6 | CURRENT_CONFIG_FILE = File.expand_path(".erdconfig", Dir.pwd) 7 | 8 | attr_reader :options 9 | 10 | def self.load(extra_config_file=nil) 11 | new.load extra_config_file 12 | end 13 | 14 | def initialize 15 | @options = {} 16 | end 17 | 18 | def load(extra_config_file=nil) 19 | load_file(USER_WIDE_CONFIG_FILE) 20 | load_file(CURRENT_CONFIG_FILE) 21 | if extra_config_file 22 | extra_config_path = File.expand_path(extra_config_file, Dir.pwd) 23 | load_file(extra_config_path) if File.exist?(extra_config_path) 24 | end 25 | 26 | @options 27 | end 28 | 29 | def self.font_names_based_on_os 30 | if use_os_x_fonts? 31 | { normal: "ArialMT", 32 | bold: "Arial BoldMT", 33 | italic: "Arial ItalicMT" } 34 | else 35 | { normal: "Arial", 36 | bold: "Arial Bold", 37 | italic: "Arial Italic" } 38 | end 39 | end 40 | 41 | private 42 | 43 | def load_file(path) 44 | if File.exist?(path) 45 | YAML.load_file(path).each do |key, value| 46 | key = key.to_sym 47 | @options[key] = normalize_value(key, value) 48 | end 49 | end 50 | end 51 | 52 | def normalize_value(key, value) 53 | case key 54 | # [,,...] | false 55 | when :attributes 56 | if value == false 57 | return value 58 | else 59 | # Comma separated string and strings in array are OK. 60 | Array(value).join(",").split(",").map { |v| v.strip.to_sym } 61 | end 62 | 63 | # 64 | when :filetype, :notation, :generator 65 | value.to_sym 66 | 67 | # [] 68 | when :only, :exclude 69 | Array(value).join(",").split(",").map { |v| v.strip } 70 | 71 | # true | false 72 | when :disconnected, :indirect, :inheritance, :markup, :polymorphism, 73 | :warn, :cluster 74 | !!value 75 | 76 | # nil | 77 | when :filename, :orientation 78 | value.nil? ? nil : value.to_s 79 | 80 | # true | false | 81 | when :title 82 | value.is_a?(String) ? value : !!value 83 | 84 | # nil | 85 | when :fonts 86 | if value 87 | Hash(value).transform_keys(&:to_sym) 88 | end 89 | 90 | else 91 | value 92 | end 93 | end 94 | 95 | def self.use_os_x_fonts? 96 | host = RbConfig::CONFIG['host_os'] 97 | return true if host == "darwin" 98 | 99 | if host.include? "darwin" 100 | darwin_version_array = host.split("darwin").last.split(".").map(&:to_i) 101 | 102 | return true if darwin_version_array[0] >= 13 103 | return true if darwin_version_array[0] == 12 && darwin_version_array[1] >= 5 104 | end 105 | 106 | false 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/rails_erd/diagram.rb: -------------------------------------------------------------------------------- 1 | require "rails_erd/domain" 2 | 3 | module RailsERD 4 | # This class is an abstract class that will process a domain model and 5 | # allows easy creation of diagrams. To implement a new diagram type, derive 6 | # from this class and override +process_entity+, +process_relationship+, 7 | # and (optionally) +save+. 8 | # 9 | # As an example, a diagram class that generates code that can be used with 10 | # yUML (https://yuml.me) can be as simple as: 11 | # 12 | # require "rails_erd/diagram" 13 | # 14 | # class YumlDiagram < RailsERD::Diagram 15 | # setup { @edges = [] } 16 | # 17 | # each_relationship do |relationship| 18 | # return if relationship.indirect? 19 | # 20 | # arrow = case 21 | # when relationship.one_to_one? then "1-1>" 22 | # when relationship.one_to_many? then "1-*>" 23 | # when relationship.many_to_many? then "*-*>" 24 | # end 25 | # 26 | # @edges << "[#{relationship.source}] #{arrow} [#{relationship.destination}]" 27 | # end 28 | # 29 | # save { @edges * "\n" } 30 | # end 31 | # 32 | # Then, to generate the diagram (example based on the domain model of Gemcutter): 33 | # 34 | # YumlDiagram.create 35 | # #=> "[Rubygem] 1-*> [Ownership] 36 | # # [Rubygem] 1-*> [Subscription] 37 | # # [Rubygem] 1-*> [Version] 38 | # # [Rubygem] 1-1> [Linkset] 39 | # # [Rubygem] 1-*> [Dependency] 40 | # # [Version] 1-*> [Dependency] 41 | # # [User] 1-*> [Ownership] 42 | # # [User] 1-*> [Subscription] 43 | # # [User] 1-*> [WebHook]" 44 | # 45 | # For another example implementation, see Diagram::Graphviz, which is the 46 | # default (and currently only) diagram type that is used by Rails ERD. 47 | # 48 | # === Options 49 | # 50 | # The following options are available and will by automatically used by any 51 | # diagram generator inheriting from this class. 52 | # 53 | # attributes:: Selects which attributes to display. Can be any combination of 54 | # +:content+, +:primary_keys+, +:foreign_keys+, +:timestamps+, or 55 | # +:inheritance+. 56 | # disconnected:: Set to +false+ to exclude entities that are not connected to other 57 | # entities. Defaults to +false+. 58 | # indirect:: Set to +false+ to exclude relationships that are indirect. 59 | # Indirect relationships are defined in Active Record with 60 | # has_many :through associations. 61 | # inheritance:: Set to +true+ to include specializations, which correspond to 62 | # Rails single table inheritance. 63 | # polymorphism:: Set to +true+ to include generalizations, which correspond to 64 | # Rails polymorphic associations. 65 | # warn:: When set to +false+, no warnings are printed to the 66 | # command line while processing the domain model. Defaults 67 | # to +true+. 68 | class Diagram 69 | class << self 70 | # Generates a new domain model based on all ActiveRecord::Base 71 | # subclasses, and creates a new diagram. Use the given options for both 72 | # the domain generation and the diagram generation. 73 | def create(options = {}) 74 | new(Domain.generate(options), options).create 75 | end 76 | 77 | protected 78 | 79 | def setup(&block) 80 | callbacks[:setup] = block 81 | end 82 | 83 | def each_entity(&block) 84 | callbacks[:each_entity] = block 85 | end 86 | 87 | def each_relationship(&block) 88 | callbacks[:each_relationship] = block 89 | end 90 | 91 | def each_specialization(&block) 92 | callbacks[:each_specialization] = block 93 | end 94 | 95 | def save(&block) 96 | callbacks[:save] = block 97 | end 98 | 99 | private 100 | 101 | def callbacks 102 | @callbacks ||= Hash.new { proc {} } 103 | end 104 | end 105 | 106 | # The options that are used to create this diagram. 107 | attr_reader :options 108 | 109 | # The domain that this diagram represents. 110 | attr_reader :domain 111 | 112 | # Create a new diagram based on the given domain. 113 | def initialize(domain, options = {}) 114 | @domain, @options = domain, RailsERD.options.merge(options) 115 | end 116 | 117 | # Generates and saves the diagram, returning the result of +save+. 118 | def create 119 | generate 120 | save 121 | end 122 | 123 | # Generates the diagram, but does not save the output. It is called 124 | # internally by Diagram#create. 125 | def generate 126 | instance_eval(&callbacks[:setup]) 127 | if options.only_recursion_depth.present? 128 | depth = options.only_recursion_depth.to_s.to_i 129 | options[:only].dup.each do |class_name| 130 | options[:only]+= recurse_into_relationships(@domain.entity_by_name(class_name), depth) 131 | end 132 | options[:only].uniq! 133 | end 134 | 135 | filtered_entities.each do |entity| 136 | instance_exec entity, filtered_attributes(entity), &callbacks[:each_entity] 137 | end 138 | 139 | filtered_specializations.each do |specialization| 140 | instance_exec specialization, &callbacks[:each_specialization] 141 | end 142 | 143 | filtered_relationships.each do |relationship| 144 | instance_exec relationship, &callbacks[:each_relationship] 145 | end 146 | end 147 | 148 | def recurse_into_relationships(entity, max_level, current_level = 0) 149 | return [] unless entity 150 | return [] if max_level == current_level 151 | 152 | relationships = entity.relationships.reject{|r| r.indirect? || r.recursive?} 153 | 154 | relationships.map do |relationship| 155 | other_entitiy = if relationship.source == entity 156 | relationship.destination 157 | else 158 | relationship.source 159 | end 160 | if other_entitiy and !other_entitiy.generalized? 161 | [other_entitiy.name] + recurse_into_relationships(other_entitiy, max_level, current_level + 1) 162 | else 163 | [] 164 | end 165 | end.flatten.uniq 166 | end 167 | 168 | def save 169 | instance_eval(&callbacks[:save]) 170 | end 171 | 172 | private 173 | 174 | def callbacks 175 | @callbacks ||= self.class.send(:callbacks) 176 | end 177 | 178 | def filtered_entities 179 | @domain.entities.reject { |entity| 180 | options.exclude.present? && [options.exclude].flatten.map(&:to_sym).include?(entity.name.to_sym) or 181 | options[:only].present? && entity.model && ![options[:only]].flatten.map(&:to_sym).include?(entity.name.to_sym) or 182 | !options.inheritance && entity.specialized? or 183 | !options.polymorphism && entity.generalized? or 184 | !options.disconnected && entity.disconnected? 185 | }.compact.tap do |entities| 186 | raise "No entities found; create your models first!" if entities.empty? 187 | end 188 | end 189 | 190 | def filtered_relationships 191 | @domain.relationships.reject { |relationship| 192 | !options.indirect && relationship.indirect? 193 | } 194 | end 195 | 196 | def filtered_specializations 197 | @domain.specializations.reject { |specialization| 198 | !options.inheritance && specialization.inheritance? or 199 | !options.polymorphism && specialization.polymorphic? 200 | } 201 | end 202 | 203 | def filtered_attributes(entity) 204 | entity.attributes.reject { |attribute| 205 | # Select attributes that satisfy the conditions in the :attributes option. 206 | !options.attributes or entity.specialized? or 207 | [*options.attributes].none? { |type| attribute.send(:"#{type.to_s.chomp('s')}?") } 208 | } 209 | end 210 | 211 | def warn(message) 212 | puts "Warning: #{message}" if options.warn 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/rails_erd/diagram/mermaid.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "rails_erd/diagram" 3 | require "erb" 4 | 5 | module RailsERD 6 | class Diagram 7 | class Mermaid < Diagram 8 | 9 | attr_accessor :graph 10 | 11 | setup do 12 | self.graph = ["classDiagram"] 13 | 14 | # hard code to RL to make it easier to view diagrams from GitHub 15 | self.graph << "\tdirection RL" 16 | end 17 | 18 | each_entity do |entity, attributes| 19 | graph << "\tclass `#{entity}`" 20 | 21 | attributes.each do | attr| 22 | graph << "\t`#{entity}` : +#{attr.type} #{attr.name}" 23 | end 24 | end 25 | 26 | each_specialization do |specialization| 27 | from, to = specialization.generalized, specialization.specialized 28 | graph << "\t<> `#{specialization.generalized}`" 29 | graph << "\t #{from.name} <|-- #{to.name}" 30 | end 31 | 32 | each_relationship do |relationship| 33 | from, to = relationship.source, relationship.destination 34 | graph << "\t`#{from.name}` #{relation_arrow(relationship)} `#{to.name}`" 35 | 36 | from.children.each do |child| 37 | graph << "\t`#{child.name}` #{relation_arrow(relationship)} `#{to.name}`" 38 | end 39 | 40 | to.children.each do |child| 41 | graph << "\t`#{from.name}` #{relation_arrow(relationship)} `#{child.name}`" 42 | end 43 | end 44 | 45 | save do 46 | raise "Saving diagram failed!\nOutput directory '#{File.dirname(filename)}' does not exist." unless File.directory?(File.dirname(filename)) 47 | 48 | File.write(filename.gsub(/\s/,"_"), graph.uniq.join("\n")) 49 | filename 50 | end 51 | 52 | def filename 53 | "#{options.filename}.mmd" 54 | end 55 | 56 | def relation_arrow(relationship) 57 | arrow_body = arrow_body relationship 58 | arrow_head = arrow_head relationship 59 | arrow_tail = arrow_tail relationship 60 | 61 | "#{arrow_tail}#{arrow_body}#{arrow_head}" 62 | end 63 | 64 | def arrow_body(relationship) 65 | relationship.indirect? ? ".." : "--" 66 | end 67 | 68 | def arrow_head(relationship) 69 | relationship.to_many? ? ">" : "" 70 | end 71 | 72 | def arrow_tail(relationship) 73 | relationship.many_to? ? "<" : "" 74 | end 75 | 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/rails_erd/diagram/templates/node.html.erb: -------------------------------------------------------------------------------- 1 | <% if options.orientation == :horizontal %>{<% end %> 2 | 3 | <%= entity.name %> 4 | 5 | <% if attributes.any? %> 6 | | 7 | 8 | <% attributes.each do |attribute| %> 9 | <%= attribute %> <%= attribute.type_description %> 10 | <% end %> 11 | 12 | <% else %> 13 | <% end %> 14 | <% if options.orientation == :horizontal %>}<% end %> 15 | -------------------------------------------------------------------------------- /lib/rails_erd/diagram/templates/node.record.erb: -------------------------------------------------------------------------------- 1 | <% if options.orientation == :horizontal %>{<% end %><%= entity.name %><% if attributes.any? %> 2 | |<% attributes.each do |attribute| %><%= 3 | attribute %> (<%= attribute.type_description %>) 4 | <% end %><% end %><% if options.orientation == :horizontal %>}<% end %> -------------------------------------------------------------------------------- /lib/rails_erd/domain.rb: -------------------------------------------------------------------------------- 1 | require "rails_erd" 2 | require "rails_erd/domain/attribute" 3 | require "rails_erd/domain/entity" 4 | require "rails_erd/domain/relationship" 5 | require "rails_erd/domain/specialization" 6 | 7 | module RailsERD 8 | # The domain describes your Rails domain model. This class is the starting 9 | # point to get information about your models. 10 | # 11 | # === Options 12 | # 13 | # The following options are available: 14 | # 15 | # warn:: When set to +false+, no warnings are printed to the 16 | # command line while processing the domain model. Defaults 17 | # to +true+. 18 | class Domain 19 | class << self 20 | # Generates a domain model object based on all loaded subclasses of 21 | # ActiveRecord::Base. Make sure your models are loaded before calling 22 | # this method. 23 | # 24 | # The +options+ hash allows you to override the default options. For a 25 | # list of available options, see RailsERD. 26 | def generate(options = {}) 27 | new ActiveRecord::Base.descendants, options 28 | end 29 | 30 | # Returns the method name to retrieve the foreign key from an 31 | # association reflection object. 32 | def foreign_key_method_name # @private :nodoc: 33 | @foreign_key_method_name ||= ActiveRecord::Reflection::AssociationReflection.method_defined?(:foreign_key) ? :foreign_key : :primary_key_name 34 | end 35 | end 36 | 37 | extend Inspectable 38 | inspection_attributes 39 | 40 | # The options that are used to generate this domain model. 41 | attr_reader :options 42 | 43 | # Create a new domain model object based on the given array of models. 44 | # The given models are assumed to be subclasses of ActiveRecord::Base. 45 | def initialize(models = [], options = {}) 46 | @source_models, @options = models, RailsERD.options.merge(options) 47 | end 48 | 49 | # Returns the domain model name, which is the name of your Rails 50 | # application or +nil+ outside of Rails. 51 | def name 52 | return unless defined?(Rails) && Rails.application 53 | 54 | if Rails.application.class.respond_to?(:module_parent) 55 | Rails.application.class.module_parent.name 56 | else 57 | Rails.application.class.parent.name 58 | end 59 | end 60 | 61 | # Returns all entities of your domain model. 62 | def entities 63 | @entities ||= Entity.from_models(self, models) 64 | end 65 | 66 | # Returns all relationships in your domain model. 67 | def relationships 68 | @relationships ||= Relationship.from_associations(self, associations) 69 | end 70 | 71 | # Returns all specializations in your domain model. 72 | def specializations 73 | @specializations ||= Specialization.from_models(self, models) 74 | end 75 | 76 | # Returns a specific entity object for the given Active Record model. 77 | def entity_by_name(name) # @private :nodoc: 78 | entity_mapping[name] 79 | end 80 | 81 | # Returns an array of relationships for the given Active Record model. 82 | def relationships_by_entity_name(name) # @private :nodoc: 83 | relationships_mapping[name] or [] 84 | end 85 | 86 | def specializations_by_entity_name(name) 87 | specializations_mapping[name] or [] 88 | end 89 | 90 | def warn(message) # @private :nodoc: 91 | puts "Warning: #{message}" if options.warn 92 | end 93 | 94 | private 95 | 96 | def entity_mapping 97 | @entity_mapping ||= {}.tap do |mapping| 98 | entities.each do |entity| 99 | mapping[entity.name] = entity 100 | end 101 | end 102 | end 103 | 104 | def relationships_mapping 105 | @relationships_mapping ||= {}.tap do |mapping| 106 | relationships.each do |relationship| 107 | (mapping[relationship.source.name] ||= []) << relationship 108 | (mapping[relationship.destination.name] ||= []) << relationship 109 | end 110 | end 111 | end 112 | 113 | def specializations_mapping 114 | @specializations_mapping ||= {}.tap do |mapping| 115 | specializations.each do |specialization| 116 | (mapping[specialization.generalized.name] ||= []) << specialization 117 | (mapping[specialization.specialized.name] ||= []) << specialization 118 | end 119 | end 120 | end 121 | 122 | def models 123 | @models ||= @source_models 124 | .reject { |model| tableless_rails_models.include?(model) } 125 | .select { |model| check_model_validity(model) } 126 | .reject { |model| check_habtm_model(model) } 127 | end 128 | 129 | # Returns Rails model classes defined in the app 130 | def rails_models 131 | %w( 132 | ActionMailbox::InboundEmail 133 | ActiveStorage::Attachment 134 | ActiveStorage::Blob 135 | ActiveStorage::VariantRecord 136 | ActionText::RichText 137 | ActionText::EncryptedRichText 138 | ).map{ |model| Object.const_get(model) rescue nil }.compact 139 | end 140 | 141 | def tableless_rails_models 142 | @tableless_rails_models ||= begin 143 | if defined? Rails 144 | rails_models.reject{ |model| model.table_exists? } 145 | else 146 | [] 147 | end 148 | end 149 | end 150 | 151 | def associations 152 | @associations ||= models.collect(&:reflect_on_all_associations).flatten.select { |assoc| check_association_validity(assoc) } 153 | end 154 | 155 | def check_model_validity(model) 156 | if model.abstract_class? || model.table_exists? 157 | if model.name.nil? 158 | raise "is anonymous class" 159 | else 160 | true 161 | end 162 | else 163 | raise "table #{model.table_name} does not exist" 164 | end 165 | rescue => e 166 | warn "Ignoring invalid model #{model.name} (#{e.message})" 167 | end 168 | 169 | def check_association_validity(association) 170 | # Raises an ActiveRecord::ActiveRecordError if the association is broken. 171 | association.check_validity! 172 | 173 | if association.options[:polymorphic] 174 | check_polymorphic_association_validity(association) 175 | else 176 | entity_name = association.klass.name # Raises NameError if the associated class cannot be found. 177 | entity_by_name(entity_name) or raise "model #{entity_name} exists, but is not included in domain" 178 | end 179 | rescue => e 180 | warn "Ignoring invalid association #{association_description(association)} (#{e.message})" 181 | end 182 | 183 | def check_polymorphic_association_validity(association) 184 | entity_name = association.class_name 185 | entity = entity_by_name(entity_name) 186 | 187 | if entity || (entity && entity.generalized?) 188 | return entity 189 | else 190 | raise("polymorphic interface #{entity_name} does not exist") 191 | end 192 | end 193 | 194 | def association_description(association) 195 | "#{association.name.inspect} on #{association.active_record}" 196 | end 197 | 198 | def check_habtm_model(model) 199 | model.name.start_with?("HABTM_") 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/rails_erd/domain/attribute.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | #-- 4 | module RailsERD 5 | class Domain 6 | # Describes an entity's attribute. Attributes correspond directly to 7 | # database columns. 8 | class Attribute 9 | TIMESTAMP_NAMES = %w{created_at created_on updated_at updated_on} # @private :nodoc: 10 | 11 | class << self 12 | def from_model(domain, model) # @private :nodoc: 13 | attributes = model.columns.collect { |column| new(domain, model, column) } 14 | attributes.sort! if RailsERD.options[:sort] 15 | 16 | if RailsERD.options[:prepend_primary] 17 | attributes = prepend_primary(model, attributes) 18 | end 19 | 20 | attributes 21 | end 22 | 23 | def prepend_primary(model, attributes) 24 | primary_key = ActiveRecord::Base.get_primary_key(model) 25 | primary = attributes.index { |column| column.name == primary_key } 26 | 27 | if primary 28 | attributes[primary], attributes[0] = attributes[0], attributes[primary] 29 | end 30 | 31 | attributes 32 | end 33 | end 34 | 35 | extend Inspectable 36 | inspection_attributes :name, :type 37 | 38 | attr_reader :column # @private :nodoc: 39 | 40 | def initialize(domain, model, column) # @private :nodoc: 41 | @domain, @model, @column = domain, model, column 42 | end 43 | 44 | # The name of the attribute, equal to the column name. 45 | def name 46 | column.name 47 | end 48 | 49 | # The type of the attribute, equal to the Rails migration type. Can be any 50 | # of +:string+, +:integer+, +:boolean+, +:text+, etc. 51 | def type 52 | column.type or column.sql_type.downcase.to_sym 53 | end 54 | 55 | # Returns +true+ if this attribute is a content column, that is, if it 56 | # is not a primary key, foreign key, timestamp, or inheritance column. 57 | def content? 58 | !primary_key? and !foreign_key? and !timestamp? and !inheritance? 59 | end 60 | 61 | # Returns +true+ if this attribute is mandatory. Mandatory attributes 62 | # either have a presence validation (+validates_presence_of+), or have a 63 | # NOT NULL database constraint. 64 | def mandatory? 65 | !column.null or @model.validators_on(name).map(&:kind).include?(:presence) 66 | end 67 | 68 | def unique? 69 | @model.validators_on(name).map(&:kind).include?(:uniqueness) 70 | end 71 | 72 | # Returns +true+ if this attribute is the primary key of the entity. 73 | def primary_key? 74 | @model.primary_key.to_s == name.to_s 75 | end 76 | 77 | # Returns +true+ if this attribute is used as a foreign key for any 78 | # relationship. 79 | def foreign_key? 80 | @domain.relationships_by_entity_name(@model.name).map(&:associations).flatten.map { |associaton| 81 | associaton.send(Domain.foreign_key_method_name).to_sym 82 | }.include?(name.to_sym) 83 | end 84 | 85 | # Returns +true+ if this attribute is used for single table inheritance. 86 | # These attributes are typically named +type+. 87 | def inheritance? 88 | @model.inheritance_column == name 89 | end 90 | 91 | # Method allows false to be set as an attributes option when making custom graphs. 92 | # It rejects all attributes when called from Diagram#filtered_attributes method 93 | def false? 94 | false 95 | end 96 | 97 | # Returns +true+ if this attribute is one of the standard 'magic' Rails 98 | # timestamp columns, being +created_at+, +updated_at+, +created_on+ or 99 | # +updated_on+. 100 | def timestamp? 101 | TIMESTAMP_NAMES.include? name 102 | end 103 | 104 | def <=>(other) # @private :nodoc: 105 | name <=> other.name 106 | end 107 | 108 | def to_s # @private :nodoc: 109 | name 110 | end 111 | 112 | # Returns a description of the attribute type. If the attribute has 113 | # a non-standard limit or if it is mandatory, this information is included. 114 | # 115 | # Example output: 116 | # :integer:: integer 117 | # :string, :limit => 255:: string 118 | # :string, :limit => 128:: string (128) 119 | # :decimal, :precision => 5, :scale => 2/tt>:: decimal (5,2) 120 | # :boolean, :null => false:: boolean * 121 | def type_description 122 | type.to_s.dup.tap do |desc| 123 | desc << " #{limit_description}" if limit_description 124 | desc << " ∗" if mandatory? && !primary_key? # Add a hair space + low asterisk (Unicode characters) 125 | desc << " U" if unique? && !primary_key? && !foreign_key? # Add U if unique but non-key 126 | desc << " PK" if primary_key? 127 | desc << " FK" if foreign_key? 128 | end 129 | end 130 | 131 | # Returns any non-standard limit for this attribute. If a column has no 132 | # limit or uses a default database limit, this method returns +nil+. 133 | def limit 134 | return if native_type == 'geometry' || native_type == 'geography' 135 | return column.limit.to_i if column.limit != native_type[:limit] and column.limit.respond_to?(:to_i) 136 | column.precision.to_i if column.precision != native_type[:precision] and column.precision.respond_to?(:to_i) 137 | end 138 | 139 | # Returns any non-standard scale for this attribute (decimal types only). 140 | def scale 141 | return column.scale.to_i if column.scale != native_type[:scale] and column.scale.respond_to?(:to_i) 142 | 0 if column.precision 143 | end 144 | 145 | # Returns a string that describes the limit for this attribute, such as 146 | # +(128)+, or +(5,2)+ for decimal types. Returns nil if no non-standard 147 | # limit was set. 148 | def limit_description # @private :nodoc: 149 | return "(#{limit},#{scale})" if limit and scale 150 | return "(#{limit})" if limit 151 | end 152 | 153 | private 154 | 155 | def native_type 156 | @model.connection.native_database_types[type] or {} 157 | end 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/rails_erd/domain/entity.rb: -------------------------------------------------------------------------------- 1 | module RailsERD 2 | class Domain 3 | # Entities represent your Active Record models. Entities may be connected 4 | # to other entities. 5 | class Entity 6 | class << self 7 | def from_models(domain, models) # @private :nodoc: 8 | (concrete_from_models(domain, models) + abstract_from_models(domain, models)).sort 9 | end 10 | 11 | private 12 | 13 | def concrete_from_models(domain, models) 14 | models.collect { |model| new(domain, model.name, model) } 15 | end 16 | 17 | def abstract_from_models(domain, models) 18 | models.collect(&:reflect_on_all_associations).flatten.collect { |association| 19 | association.options[:as].to_s.classify if association.options[:as] 20 | }.flatten.compact.uniq.collect { |name| new(domain, name) } 21 | end 22 | end 23 | 24 | extend Inspectable 25 | inspection_attributes :model 26 | 27 | # The domain in which this entity resides. 28 | attr_reader :domain 29 | 30 | # The Active Record model that this entity corresponds to. 31 | attr_reader :model 32 | 33 | # The name of this entity. Equal to the class name of the corresponding 34 | # model (for concrete entities) or given name (for abstract entities). 35 | attr_reader :name 36 | 37 | def initialize(domain, name, model = nil) # @private :nodoc: 38 | @domain, @name, @model = domain, name, model 39 | end 40 | 41 | # Returns an array of attributes for this entity. 42 | def attributes 43 | @attributes ||= generalized? ? [] : Attribute.from_model(domain, model) 44 | end 45 | 46 | # Returns an array of all relationships that this entity has with other 47 | # entities in the domain model. 48 | def relationships 49 | domain.relationships_by_entity_name(name) 50 | end 51 | 52 | # Returns +true+ if this entity has any relationships with other models, 53 | # +false+ otherwise. 54 | def connected? 55 | relationships.any? 56 | end 57 | 58 | # Returns +true+ if this entity has no relationships with any other models, 59 | # +false+ otherwise. Opposite of +connected?+. 60 | def disconnected? 61 | relationships.none? 62 | end 63 | 64 | # Returns +true+ if this entity is a generalization, which does not 65 | # correspond with a database table. Generalized entities are either 66 | # models that are defined as +abstract_class+ or they are constructed 67 | # from polymorphic interfaces. Any +has_one+ or +has_many+ association 68 | # that defines a polymorphic interface with :as => :name will 69 | # lead to a generalized entity to be created. 70 | def generalized? 71 | !model or !!model.abstract_class? 72 | end 73 | 74 | # Returns +true+ if this entity descends from another entity, and is 75 | # represented in the same table as its parent. In Rails this concept is 76 | # referred to as single-table inheritance. In entity-relationship 77 | # diagrams it is called specialization. 78 | def specialized? 79 | !!model and !model.descends_from_active_record? 80 | end 81 | 82 | # Returns +true+ if this entity does not correspond directly with a 83 | # database table (if and only if the entity is specialized or 84 | # generalized). 85 | def virtual? 86 | generalized? or specialized? 87 | end 88 | alias_method :abstract?, :virtual? 89 | 90 | # Returns all child entities, if this is a generalized entity. 91 | def children 92 | @children ||= domain.specializations_by_entity_name(name).map(&:specialized) 93 | end 94 | 95 | def namespace 96 | $1 if name.match(/(.*)::.*/) 97 | end 98 | 99 | def to_s # @private :nodoc: 100 | name 101 | end 102 | 103 | def <=>(other) # @private :nodoc: 104 | self.name <=> other.name 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/rails_erd/domain/relationship.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | require "active_support/core_ext/module/delegation" 3 | require "rails_erd/domain/relationship/cardinality" 4 | 5 | module RailsERD 6 | class Domain 7 | # Describes a relationship between two entities. A relationship is detected 8 | # based on Active Record associations. One relationship may represent more 9 | # than one association, however. Related associations are grouped together. 10 | # Associations are related if they share the same foreign key, or the same 11 | # join table in the case of many-to-many associations. 12 | class Relationship 13 | N = Cardinality::N 14 | 15 | class << self 16 | def from_associations(domain, associations) # @private :nodoc: 17 | assoc_groups = associations.group_by { |assoc| association_identity(assoc) } 18 | assoc_groups.collect { |_, assoc_group| new(domain, assoc_group.to_a) } 19 | end 20 | 21 | private 22 | 23 | def association_identity(association) 24 | Set[association_owner(association), association_target(association)] 25 | end 26 | 27 | def association_identifier(association) 28 | if association.macro == :has_and_belongs_to_many 29 | # Rails 4+ supports the join_table method, and doesn't expose it 30 | # as an option if it's an implicit default. 31 | (association.respond_to?(:join_table) && association.join_table) || association.options[:join_table] 32 | else 33 | association.options[:through] || association.send(Domain.foreign_key_method_name).to_s 34 | end 35 | end 36 | 37 | def association_owner(association) 38 | association.options[:as] ? association.options[:as].to_s.classify : association.active_record.name 39 | end 40 | 41 | def association_target(association) 42 | association.options[:polymorphic] ? association.class_name : association.klass.name 43 | end 44 | end 45 | 46 | extend Inspectable 47 | inspection_attributes :source, :destination 48 | 49 | # The domain in which this relationship is defined. 50 | attr_reader :domain 51 | 52 | # The source entity. It corresponds to the model that has defined a 53 | # +has_one+ or +has_many+ association with the other model. 54 | attr_reader :source 55 | 56 | # The destination entity. It corresponds to the model that has defined 57 | # a +belongs_to+ association with the other model. 58 | attr_reader :destination 59 | 60 | delegate :one_to_one?, :one_to_many?, :many_to_many?, :source_optional?, 61 | :destination_optional?, :to => :cardinality 62 | 63 | def initialize(domain, associations) # @private :nodoc: 64 | @domain = domain 65 | @reverse_associations, @forward_associations = partition_associations(associations) 66 | 67 | assoc = @forward_associations.first || @reverse_associations.first 68 | @source = @domain.entity_by_name(self.class.send(:association_owner, assoc)) 69 | @destination = @domain.entity_by_name(self.class.send(:association_target, assoc)) 70 | @source, @destination = @destination, @source if assoc.belongs_to? 71 | end 72 | 73 | # Returns all Active Record association objects that describe this 74 | # relationship. 75 | def associations 76 | @forward_associations + @reverse_associations 77 | end 78 | 79 | # Returns the cardinality of this relationship. 80 | def cardinality 81 | @cardinality ||= begin 82 | reverse_max = any_habtm?(associations) ? N : 1 83 | forward_range = associations_range(@forward_associations, N) 84 | reverse_range = associations_range(@reverse_associations, reverse_max) 85 | Cardinality.new(reverse_range, forward_range) 86 | end 87 | end 88 | 89 | # Indicates if a relationship is indirect, that is, if it is defined 90 | # through other relationships. Indirect relationships are created in 91 | # Rails with has_many :through or has_one :through 92 | # association macros. 93 | def indirect? 94 | !@forward_associations.empty? and @forward_associations.all?(&:through_reflection) 95 | end 96 | 97 | # Indicates whether or not the relationship is defined by two inverse 98 | # associations (e.g. a +has_many+ and a corresponding +belongs_to+ 99 | # association). 100 | def mutual? 101 | @forward_associations.any? and @reverse_associations.any? 102 | end 103 | 104 | # Indicates whether or not this relationship connects an entity with itself. 105 | def recursive? 106 | @source == @destination 107 | end 108 | 109 | # Indicates whether the destination cardinality class of this relationship 110 | # is equal to one. This is +true+ for one-to-one relationships only. 111 | def to_one? 112 | cardinality.cardinality_class[1] == 1 113 | end 114 | 115 | # Indicates whether the destination cardinality class of this relationship 116 | # is equal to infinity. This is +true+ for one-to-many or 117 | # many-to-many relationships only. 118 | def to_many? 119 | cardinality.cardinality_class[1] != 1 120 | end 121 | 122 | # Indicates whether the source cardinality class of this relationship 123 | # is equal to one. This is +true+ for one-to-one or 124 | # one-to-many relationships only. 125 | def one_to? 126 | cardinality.cardinality_class[0] == 1 127 | end 128 | 129 | # Indicates whether the source cardinality class of this relationship 130 | # is equal to infinity. This is +true+ for many-to-many relationships only. 131 | def many_to? 132 | cardinality.cardinality_class[0] != 1 133 | end 134 | 135 | # The strength of a relationship is equal to the number of associations 136 | # that describe it. 137 | def strength 138 | if source.generalized? then 1 else associations.size end 139 | end 140 | 141 | def <=>(other) # @private :nodoc: 142 | (source.name <=> other.source.name).nonzero? or (destination.name <=> other.destination.name) 143 | end 144 | 145 | private 146 | 147 | def partition_associations(associations) 148 | if any_habtm?(associations) 149 | # Many-to-many associations don't have a clearly defined direction. 150 | # We sort by name and use the first model as the source. 151 | source = associations.map(&:active_record).sort_by(&:name).first 152 | associations.partition { |association| association.active_record != source } 153 | else 154 | associations.partition(&:belongs_to?) 155 | end 156 | end 157 | 158 | def associations_range(associations, absolute_max) 159 | # The minimum of the range is the maximum value of each association 160 | # minimum. If there is none, it is zero by definition. The reasoning is 161 | # that from all associations, if only one has a required minimum, then 162 | # this side of the relationship has a cardinality of at least one. 163 | min = associations.map { |assoc| association_minimum(assoc) }.max || 0 164 | 165 | # The maximum of the range is the maximum value of each association 166 | # maximum. If there is none, it is equal to the absolute maximum. If 167 | # only one association has a high cardinality on this side, the 168 | # relationship itself has the same maximum cardinality. 169 | max = associations.map { |assoc| association_maximum(assoc) }.max || absolute_max 170 | 171 | min..max 172 | end 173 | 174 | def association_minimum(association) 175 | minimum = association_validators(:presence, association).any? || 176 | foreign_key_required?(association) ? 1 : 0 177 | length_validators = association_validators(:length, association) 178 | length_validators.map { |v| v.options[:minimum] }.compact.max or minimum 179 | end 180 | 181 | def association_maximum(association) 182 | maximum = association.collection? ? N : 1 183 | length_validators = association_validators(:length, association) 184 | length_validators.map { |v| v.options[:maximum] }.compact.min or maximum 185 | end 186 | 187 | def association_validators(kind, association) 188 | association.active_record.validators_on(association.name).select { |v| v.kind == kind } 189 | end 190 | 191 | def any_habtm?(associations) 192 | associations.any? { |association| association.macro == :has_and_belongs_to_many } 193 | end 194 | 195 | def foreign_key_required?(association) 196 | if !association.active_record.abstract_class? and association.belongs_to? 197 | column = association.active_record.columns_hash[association.send(Domain.foreign_key_method_name)] and !column.null 198 | end 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/rails_erd/domain/relationship/cardinality.rb: -------------------------------------------------------------------------------- 1 | module RailsERD 2 | class Domain 3 | class Relationship 4 | class Cardinality 5 | extend Inspectable 6 | inspection_attributes :source_range, :destination_range 7 | 8 | N = Infinity = 1.0/0 # And beyond. 9 | 10 | CLASSES = { 11 | [1, 1] => :one_to_one, 12 | [1, N] => :one_to_many, 13 | [N, 1] => :many_to_one, 14 | [N, N] => :many_to_many 15 | } # @private :nodoc: 16 | 17 | # Returns a range that indicates the source (left) cardinality. 18 | attr_reader :source_range 19 | 20 | # Returns a range that indicates the destination (right) cardinality. 21 | attr_reader :destination_range 22 | 23 | # Create a new cardinality based on a source range and a destination 24 | # range. These ranges describe which number of values are valid. 25 | def initialize(source_range, destination_range) # @private :nodoc: 26 | @source_range = compose_range(source_range) 27 | @destination_range = compose_range(destination_range) 28 | end 29 | 30 | # Returns the name of this cardinality, based on its two cardinal 31 | # numbers (for source and destination). Can be any of 32 | # +:one_to_one:+, +:one_to_many+, or +:many_to_many+. The name 33 | # +:many_to_one+ also exists, but Rails ERD always normalises these 34 | # kinds of relationships by inverting them, so they become 35 | # +:one_to_many+ associations. 36 | # 37 | # You can also call the equivalent method with a question mark, which 38 | # will return true if the name corresponds to that method. For example: 39 | # 40 | # cardinality.one_to_one? 41 | # #=> true 42 | # cardinality.one_to_many? 43 | # #=> false 44 | def name 45 | CLASSES[cardinality_class] 46 | end 47 | 48 | # Returns +true+ if the source (left side) is not mandatory. 49 | def source_optional? 50 | source_range.first < 1 51 | end 52 | 53 | # Returns +true+ if the destination (right side) is not mandatory. 54 | def destination_optional? 55 | destination_range.first < 1 56 | end 57 | 58 | # Returns the inverse cardinality. Destination becomes source, source 59 | # becomes destination. 60 | def inverse 61 | self.class.new destination_range, source_range 62 | end 63 | 64 | CLASSES.each do |cardinality_class, name| 65 | class_eval <<-RUBY 66 | def #{name}? 67 | cardinality_class == #{cardinality_class.inspect} 68 | end 69 | RUBY 70 | end 71 | 72 | def ==(other) # @private :nodoc: 73 | source_range == other.source_range and destination_range == other.destination_range 74 | end 75 | 76 | def <=>(other) # @private :nodoc: 77 | (cardinality_class <=> other.cardinality_class).nonzero? or 78 | compare_with(other) { |x| x.source_range.first + x.destination_range.first }.nonzero? or 79 | compare_with(other) { |x| x.source_range.last + x.destination_range.last }.nonzero? or 80 | compare_with(other) { |x| x.source_range.last }.nonzero? or 81 | compare_with(other) { |x| x.destination_range.last } 82 | end 83 | 84 | # Returns an array with the cardinality classes for the source and 85 | # destination of this cardinality. Possible return values are: 86 | # [1, 1], [1, N], [N, N], and (in theory) 87 | # [N, 1]. 88 | def cardinality_class 89 | [source_cardinality_class, destination_cardinality_class] 90 | end 91 | 92 | protected 93 | 94 | # The cardinality class of the source (left side). Either +1+ or +Infinity+. 95 | def source_cardinality_class 96 | source_range.last == 1 ? 1 : N 97 | end 98 | 99 | # The cardinality class of the destination (right side). Either +1+ or +Infinity+. 100 | def destination_cardinality_class 101 | destination_range.last == 1 ? 1 : N 102 | end 103 | 104 | private 105 | 106 | def compose_range(r) 107 | return r..r if r.kind_of?(Integer) && r > 0 108 | return (r.begin)..(r.end - 1) if r.exclude_end? 109 | r 110 | end 111 | 112 | def compare_with(other, &block) 113 | yield(self) <=> yield(other) 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/rails_erd/domain/specialization.rb: -------------------------------------------------------------------------------- 1 | module RailsERD 2 | class Domain 3 | # Describes the specialization of an entity. Specialized entities correspond 4 | # to inheritance or polymorphism. In Rails, specialization is referred to 5 | # as single table inheritance, while generalization is referred to as 6 | # polymorphism or abstract classes. 7 | class Specialization 8 | class << self 9 | def from_models(domain, models) # @private :nodoc: 10 | models = polymorphic_from_models(domain, models) + 11 | inheritance_from_models(domain, models) + 12 | abstract_from_models(domain, models) 13 | models.sort 14 | end 15 | 16 | private 17 | 18 | def polymorphic_from_models(domain, models) 19 | models.collect(&:reflect_on_all_associations).flatten.collect { |association| 20 | [association.options[:as].to_s.classify, association.active_record.name] if association.options[:as] 21 | }.compact.uniq.collect { |names| 22 | new(domain, domain.entity_by_name(names.first), domain.entity_by_name(names.last)) 23 | } 24 | end 25 | 26 | def inheritance_from_models(domain, models) 27 | models.reject(&:descends_from_active_record?).collect { |model| 28 | new(domain, domain.entity_by_name(model.base_class.name), domain.entity_by_name(model.name)) 29 | } 30 | end 31 | 32 | def abstract_from_models(domain, models) 33 | abstract_classes = models.select(&:abstract_class?) 34 | direct_descendants = if ActiveRecord.version >= Gem::Version.new("7.0.0") 35 | abstract_classes.collect(&:subclasses) 36 | else 37 | abstract_classes.collect(&:direct_descendants) 38 | end 39 | 40 | direct_descendants.flatten.collect { |model| 41 | new(domain, domain.entity_by_name(model.superclass.name), domain.entity_by_name(model.name)) 42 | } 43 | end 44 | end 45 | 46 | extend Inspectable 47 | inspection_attributes :generalized, :specialized 48 | 49 | # The domain in which this specialization is defined. 50 | attr_reader :domain 51 | 52 | # The source entity. 53 | attr_reader :generalized 54 | 55 | # The destination entity. 56 | attr_reader :specialized 57 | 58 | def initialize(domain, generalized, specialized) # @private :nodoc: 59 | @domain = domain 60 | @generalized = generalized || NullGeneralized.new 61 | @specialized = specialized || NullSpecialization.new 62 | end 63 | 64 | def generalization? 65 | generalized.generalized? 66 | end 67 | alias_method :polymorphic?, :generalization? 68 | 69 | def specialization? 70 | !generalization? 71 | end 72 | alias_method :inheritance?, :specialization? 73 | 74 | def <=>(other) # @private :nodoc: 75 | (generalized.name <=> other.generalized.name).nonzero? or (specialized.name <=> other.specialized.name) 76 | end 77 | end 78 | 79 | class NullSpecialization 80 | def name 81 | "" 82 | end 83 | def generalized? 84 | false 85 | end 86 | end 87 | 88 | class NullGeneralized 89 | def name 90 | "" 91 | end 92 | def generalized? 93 | true 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/rails_erd/railtie.rb: -------------------------------------------------------------------------------- 1 | module RailsERD 2 | # Rails ERD integrates with Rails 3. If you add it to your +Gemfile+, you 3 | # will gain a Rake task called +erd+, which you can use to generate diagrams 4 | # of your domain model. 5 | class Railtie < Rails::Railtie 6 | rake_tasks do 7 | load "rails_erd/tasks.rake" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/rails_erd/tasks.rake: -------------------------------------------------------------------------------- 1 | require 'graphviz/utils' 2 | 3 | module ErdRakeHelper 4 | def say(message) 5 | puts message unless Rake.application.options.silent 6 | end 7 | end 8 | 9 | namespace :erd do 10 | task :check_dependencies do 11 | if RailsERD.options.generator == :graphviz 12 | include GraphViz::Utils 13 | unless find_executable("dot", nil) 14 | raise "#{RailsERD.options.generator} Unable to find GraphViz's \"dot\" executable. Please " \ 15 | "visit https://voormedia.github.io/rails-erd/install.html for installation instructions." 16 | end 17 | end 18 | end 19 | 20 | task :options do 21 | (RailsERD.options.keys.map(&:to_s) & ENV.keys).each do |option| 22 | RailsERD.options[option.to_sym] = case ENV[option] 23 | when "true", "yes" then true 24 | when "false", "no" then false 25 | when /,/ then ENV[option].split(/\s*,\s*/) 26 | when /^\d+$/ then ENV[option].to_i 27 | else 28 | if option == 'only' 29 | [ENV[option]] 30 | else 31 | ENV[option].to_sym 32 | end 33 | end 34 | end 35 | end 36 | 37 | task :load_models do 38 | include ErdRakeHelper 39 | 40 | say "Loading application environment..." 41 | Rake::Task[:environment].invoke 42 | 43 | say "Loading code in search of Active Record models..." 44 | begin 45 | Rails.application.eager_load! 46 | 47 | if Rails.application.respond_to?(:config) && !Rails.application.config.nil? 48 | Rails.application.config.eager_load_namespaces.each(&:eager_load!) if Rails.application.config.respond_to?(:eager_load_namespaces) 49 | end 50 | rescue Exception => err 51 | if Rake.application.options.trace 52 | raise 53 | else 54 | trace = Rails.backtrace_cleaner.clean(err.backtrace) 55 | error = (["Loading models failed!\nError occurred while loading application: #{err} (#{err.class})"] + trace).join("\n ") 56 | raise error 57 | end 58 | end 59 | 60 | raise "Active Record was not loaded." unless defined? ActiveRecord 61 | end 62 | 63 | task :generate => [:options, :check_dependencies, :load_models] do 64 | include ErdRakeHelper 65 | 66 | say "Generating Entity-Relationship Diagram for #{ActiveRecord::Base.descendants.length} models..." 67 | 68 | file = case RailsERD.options.generator 69 | when :mermaid 70 | require "rails_erd/diagram/mermaid" 71 | RailsERD::Diagram::Mermaid.create 72 | when :graphviz 73 | require "rails_erd/diagram/graphviz" 74 | RailsERD::Diagram::Graphviz.create 75 | else 76 | raise "Unknown generator: #{RailsERD.options.generator}" 77 | end 78 | 79 | 80 | say "Done! Saved diagram to ./#{file}" 81 | end 82 | end 83 | 84 | desc "Generate an Entity-Relationship Diagram based on your models" 85 | task :erd => "erd:generate" 86 | -------------------------------------------------------------------------------- /lib/rails_erd/version.rb: -------------------------------------------------------------------------------- 1 | module RailsERD 2 | VERSION = "1.7.2" 3 | BANNER = "RailsERD #{VERSION}" 4 | end 5 | -------------------------------------------------------------------------------- /lib/tasks/auto_generate_diagram.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | task :migrate do 3 | ERDGraph::Migration.update_model 4 | end 5 | 6 | namespace :migrate do 7 | [:change, :up, :down, :reset, :redo].each do |t| 8 | task t do 9 | ERDGraph::Migration.update_model 10 | end 11 | end 12 | end 13 | end 14 | 15 | module ERDGraph 16 | class Migration 17 | def self.update_model 18 | Rake::Task['erd'].invoke 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /rails-erd.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "rails_erd/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "rails-erd" 6 | s.version = RailsERD::VERSION 7 | s.authors = ["Rolf Timmermans", "Kerri Miller"] 8 | s.email = ["r.timmermans@voormedia.com", "kerrizor@kerrizor.com"] 9 | s.homepage = "https://github.com/voormedia/rails-erd" 10 | s.summary = "Entity-relationship diagram for your Rails models." 11 | s.description = "Automatically generate an entity-relationship diagram (ERD) for your Rails models." 12 | s.license = "MIT" 13 | 14 | s.required_ruby_version = '>= 2.2' 15 | 16 | s.add_runtime_dependency "activerecord", ">= 4.2" 17 | s.add_runtime_dependency "activesupport", ">= 4.2" 18 | s.add_runtime_dependency "ruby-graphviz", "~> 1.2" 19 | s.add_runtime_dependency "choice", "~> 0.2.0" 20 | 21 | s.add_development_dependency "pry" 22 | s.add_development_dependency "pry-nav" 23 | 24 | s.files = `git ls-files -- {bin,lib,test}/* CHANGES.rdoc LICENSE Rakefile README.md`.split("\n") 25 | s.test_files = `git ls-files -- test/*`.split("\n") 26 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 27 | s.require_paths = ["lib"] 28 | end 29 | -------------------------------------------------------------------------------- /test/support_files/erdconfig.another_example: -------------------------------------------------------------------------------- 1 | attributes: 2 | - primary_key 3 | 4 | -------------------------------------------------------------------------------- /test/support_files/erdconfig.example: -------------------------------------------------------------------------------- 1 | attributes: 2 | - content 3 | - foreign_key 4 | - inheritance 5 | - false 6 | disconnected: true 7 | filename: erd 8 | filetype: pdf 9 | indirect: true 10 | inheritance: false 11 | markup: true 12 | notation: simple 13 | orientation: horizontal 14 | polymorphism: false 15 | warn: true 16 | title: sample title 17 | exclude: 18 | only: 19 | 20 | -------------------------------------------------------------------------------- /test/support_files/erdconfig.exclude.example: -------------------------------------------------------------------------------- 1 | attributes: 2 | - content 3 | - foreign_key 4 | - inheritance 5 | - false 6 | disconnected: true 7 | filename: erd 8 | filetype: pdf 9 | indirect: true 10 | inheritance: false 11 | markup: true 12 | notation: simple 13 | orientation: horizontal 14 | polymorphism: false 15 | warn: true 16 | title: sample title 17 | exclude: Book,Author 18 | only: 19 | 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | require 'pry' 4 | require 'pry-nav' 5 | 6 | require "active_record" 7 | 8 | require "minitest/autorun" 9 | require 'mocha/minitest' 10 | 11 | require "rails_erd/domain" 12 | 13 | ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:" 14 | 15 | if ActiveSupport::TestCase.respond_to?(:test_order=) 16 | ActiveSupport::TestCase.test_order = :random 17 | end 18 | 19 | # Patch to make Rails 6.1 work. 20 | module Kernel 21 | # class_eval on an object acts like singleton_class.class_eval. 22 | def class_eval(*args, &block) 23 | singleton_class.class_eval(*args, &block) 24 | end 25 | end 26 | 27 | class ActiveSupport::TestCase 28 | include RailsERD 29 | 30 | setup :reset_config_file 31 | teardown :reset_domain 32 | 33 | def create_table(table, columns = {}, pk = nil) 34 | opts = if pk then { :primary_key => pk } else { :id => false } end 35 | ActiveRecord::Schema.instance_eval do 36 | suppress_messages do 37 | unless ActiveRecord::Base.connection.table_exists?(table) 38 | create_table table, **opts do |t| 39 | columns.each do |column, type| 40 | t.send type, column 41 | end 42 | end 43 | end 44 | end 45 | end 46 | ActiveRecord::Base.clear_cache! 47 | end 48 | 49 | def add_column(*args) 50 | ActiveRecord::Schema.instance_eval do 51 | suppress_messages do 52 | opts = args.slice!(3) || {} 53 | add_column(*args, **opts) 54 | end 55 | end 56 | ActiveRecord::Base.clear_cache! 57 | end 58 | 59 | def create_module_model(full_name,*args,&block) 60 | superklass = args.first.kind_of?(Class) ? args.shift : ActiveRecord::Base 61 | 62 | names = full_name.split('::') 63 | 64 | parent_module = names[0..-1].inject(Object) do |parent,child| 65 | parent = parent.const_set(child.to_sym, Module.new) 66 | end 67 | 68 | parent_module ||= Object 69 | name = names.last 70 | 71 | columns = args.first || {} 72 | klass = parent_module.const_set name.to_sym, Class.new(superklass) 73 | konstant = parent_module.const_get(name.to_sym) 74 | 75 | if superklass == ActiveRecord::Base || superklass.abstract_class? 76 | create_table konstant.table_name, columns, konstant.primary_key rescue nil 77 | end 78 | klass.class_eval(&block) if block_given? 79 | konstant 80 | end 81 | 82 | def create_model(name, *args, &block) 83 | superklass = args.first.kind_of?(Class) ? args.shift : ActiveRecord::Base 84 | columns = args.first || {} 85 | klass = Object.const_set name.to_sym, Class.new(superklass) 86 | 87 | if superklass == ActiveRecord::Base || superklass.abstract_class? 88 | create_table Object.const_get(name.to_sym).table_name, columns, Object.const_get(name.to_sym).primary_key 89 | end 90 | klass.class_eval(&block) if block_given? 91 | Object.const_get(name.to_sym) 92 | end 93 | 94 | def create_models(*names) 95 | names.each do |name| 96 | create_model name 97 | end 98 | end 99 | 100 | def collect_stdout 101 | stdout = $stdout 102 | $stdout = StringIO.new 103 | yield 104 | $stdout.rewind 105 | $stdout.read 106 | ensure 107 | $stdout = stdout 108 | end 109 | 110 | def create_simple_domain 111 | create_model "Beer", :bar => :references do 112 | belongs_to :bar 113 | end 114 | create_model "Bar" 115 | end 116 | 117 | def create_one_to_one_assoc_domain 118 | create_model "One" do 119 | has_one :other 120 | end 121 | create_model "Other", :one => :references do 122 | belongs_to :one 123 | end 124 | end 125 | 126 | def create_one_to_many_assoc_domain 127 | create_model "One" do 128 | has_many :many 129 | end 130 | create_model "Many", :one => :references do 131 | belongs_to :one 132 | end 133 | end 134 | 135 | def create_many_to_many_assoc_domain 136 | create_model "Many" do 137 | has_and_belongs_to_many :more 138 | end 139 | create_model "More" do 140 | has_and_belongs_to_many :many 141 | end 142 | create_table "manies_mores", :many_id => :integer, :more_id => :integer 143 | end 144 | 145 | def create_specialization 146 | create_model "Beverage", :type => :string 147 | create_model "Beer", Beverage 148 | end 149 | 150 | def create_polymorphic_generalization 151 | create_model "Cannon" 152 | create_model "Galleon" do 153 | has_many :cannons, :as => :defensible 154 | end 155 | end 156 | 157 | def create_abstract_generalization 158 | create_model "Structure" do 159 | self.abstract_class = true 160 | end 161 | create_model "Palace", Structure 162 | end 163 | 164 | private 165 | 166 | def reset_config_file 167 | RailsERD::Config.send :remove_const, :USER_WIDE_CONFIG_FILE 168 | RailsERD::Config.send :const_set, :USER_WIDE_CONFIG_FILE, 169 | File.expand_path("../../examples/erdconfig.not_exists", __FILE__) 170 | 171 | RailsERD::Config.send :remove_const, :CURRENT_CONFIG_FILE 172 | RailsERD::Config.send :const_set, :CURRENT_CONFIG_FILE, 173 | File.expand_path("../../examples/erdconfig.not_exists", __FILE__) 174 | 175 | RailsERD.options = RailsERD.default_options.merge(Config.load) 176 | end 177 | 178 | def name_to_object_symbol_pairs(name) 179 | parts = name.to_s.split('::') 180 | 181 | return [] if parts.first == '' || parts.count == 0 182 | 183 | parts[1..-1].inject([[Object, parts.first.to_sym]]) do |pairs,string| 184 | last_parent, last_child = pairs.last 185 | # Fixes for Rails 6. No idea if this is actually correct as I can't decipher what the heck is going on in this 186 | # code. 187 | if last_child == :ActiveRecord || last_child == :primary 188 | break [] 189 | end 190 | 191 | break pairs unless last_parent.const_defined?(last_child) 192 | 193 | next_parent = last_parent.const_get(last_child) 194 | next_child = string.to_sym 195 | pairs << [next_parent, next_child] 196 | end 197 | end 198 | 199 | def remove_fully_qualified_constant(name) 200 | pairs = name_to_object_symbol_pairs(name) 201 | pairs.reverse.each do |parent, child| 202 | parent.send(:remove_const,child) if parent.const_defined?(child) 203 | end 204 | end 205 | 206 | def reset_domain 207 | if defined? ActiveRecord 208 | ActiveRecord::Base.descendants.each do |model| 209 | next if model.name == "ActiveRecord::InternalMetadata" 210 | model.reset_column_information 211 | remove_fully_qualified_constant(model.name) 212 | end 213 | 214 | tables_and_views.each do |table| 215 | ActiveRecord::Base.connection.drop_table table 216 | end 217 | 218 | if ActiveRecord.version >= Gem::Version.new("7.0.0") 219 | ActiveSupport::DescendantsTracker.clear(ActiveRecord::Base.subclasses) 220 | elsif ActiveRecord.version >= Gem::Version.new("6.0.0.rc1") 221 | cv = ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants) 222 | cv.delete(ActiveRecord::Base) 223 | ActiveSupport::DescendantsTracker.class_variable_set(:@@direct_descendants, cv) 224 | ActiveSupport::Dependencies::Reference.clear! 225 | else 226 | ActiveRecord::Base.direct_descendants.clear 227 | ActiveSupport::Dependencies::Reference.clear! 228 | end 229 | 230 | ActiveRecord::Base.clear_cache! 231 | end 232 | end 233 | 234 | def tables_and_views 235 | if ActiveRecord::VERSION::MAJOR >= 5 236 | ActiveRecord::Base.connection.data_sources 237 | else 238 | ActiveRecord::Base.connection.tables 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /test/unit/cardinality_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 2 | 3 | class CardinalityTest < ActiveSupport::TestCase 4 | def setup 5 | @n = Domain::Relationship::Cardinality::N 6 | end 7 | 8 | # Cardinality ============================================================== 9 | test "inspect should show source and destination ranges" do 10 | assert_match %r{#}, 11 | Domain::Relationship::Cardinality.new(1, 1..@n).inspect 12 | end 13 | 14 | # Cardinality construction ================================================= 15 | test "new should return cardinality object" do 16 | assert_kind_of Domain::Relationship::Cardinality, Domain::Relationship::Cardinality.new(1, 1..@n) 17 | end 18 | 19 | # Cardinality properties =================================================== 20 | test "source_optional should return true if source range starts at zero" do 21 | assert_equal true, Domain::Relationship::Cardinality.new(0..1, 1).source_optional? 22 | end 23 | 24 | test "source_optional should return false if source range starts at one or more" do 25 | assert_equal false, Domain::Relationship::Cardinality.new(1..2, 0..1).source_optional? 26 | end 27 | 28 | test "destination_optional should return true if destination range starts at zero" do 29 | assert_equal true, Domain::Relationship::Cardinality.new(1, 0..1).destination_optional? 30 | end 31 | 32 | test "destination_optional should return false if destination range starts at one or more" do 33 | assert_equal false, Domain::Relationship::Cardinality.new(0..1, 1..2).destination_optional? 34 | end 35 | 36 | test "inverse should return inverse cardinality" do 37 | assert_equal Domain::Relationship::Cardinality.new(23..45, 0..15), Domain::Relationship::Cardinality.new(0..15, 23..45).inverse 38 | end 39 | 40 | # Cardinality equality ===================================================== 41 | test "cardinalities are equal if they have the same boundaries" do 42 | assert_equal Domain::Relationship::Cardinality.new(1, 1..Domain::Relationship::Cardinality::N), 43 | Domain::Relationship::Cardinality.new(1, 1..Domain::Relationship::Cardinality::N) 44 | end 45 | 46 | test "cardinalities are not equal if they have a different source range" do 47 | assert_not_equal Domain::Relationship::Cardinality.new(0..1, 1..Domain::Relationship::Cardinality::N), 48 | Domain::Relationship::Cardinality.new(1..1, 1..Domain::Relationship::Cardinality::N) 49 | end 50 | 51 | test "cardinalities are not equal if they have a different destination range" do 52 | assert_not_equal Domain::Relationship::Cardinality.new(0..1, 1..Domain::Relationship::Cardinality::N), 53 | Domain::Relationship::Cardinality.new(0..1, 2..Domain::Relationship::Cardinality::N) 54 | end 55 | 56 | # Cardinal names =========================================================== 57 | test "one_to_one should return true if source and destination are exactly one" do 58 | assert_equal true, Domain::Relationship::Cardinality.new(1, 1).one_to_one? 59 | end 60 | 61 | test "one_to_one should return true if source and destination range are less than or equal to one" do 62 | assert_equal true, Domain::Relationship::Cardinality.new(0..1, 0..1).one_to_one? 63 | end 64 | 65 | test "one_to_one should return false if source range upper limit is more than one" do 66 | assert_equal false, Domain::Relationship::Cardinality.new(0..15, 0..1).one_to_one? 67 | end 68 | 69 | test "one_to_one should return false if destination range upper limit is more than one" do 70 | assert_equal false, Domain::Relationship::Cardinality.new(0..1, 0..15).one_to_one? 71 | end 72 | 73 | test "one_to_many should return true if source is exactly one and destination is higher than one" do 74 | assert_equal true, Domain::Relationship::Cardinality.new(1, 15).one_to_many? 75 | end 76 | 77 | test "one_to_many should return true if source is less than or equal to one and destination is higher than one" do 78 | assert_equal true, Domain::Relationship::Cardinality.new(0..1, 0..15).one_to_many? 79 | end 80 | 81 | test "one_to_many should return false if source range upper limit is more than one" do 82 | assert_equal false, Domain::Relationship::Cardinality.new(0..15, 0..15).one_to_many? 83 | end 84 | 85 | test "one_to_many should return false if destination range upper limit is one" do 86 | assert_equal false, Domain::Relationship::Cardinality.new(0..1, 1).one_to_many? 87 | end 88 | 89 | test "many_to_many should return true if source and destination are higher than one" do 90 | assert_equal true, Domain::Relationship::Cardinality.new(15, 15).many_to_many? 91 | end 92 | 93 | test "many_to_many should return true if source and destination upper limits are higher than one" do 94 | assert_equal true, Domain::Relationship::Cardinality.new(0..15, 0..15).many_to_many? 95 | end 96 | 97 | test "many_to_many should return false if source range upper limit is is one" do 98 | assert_equal false, Domain::Relationship::Cardinality.new(1, 0..15).many_to_many? 99 | end 100 | 101 | test "many_to_many should return false if destination range upper limit is one" do 102 | assert_equal false, Domain::Relationship::Cardinality.new(0..1, 1).many_to_many? 103 | end 104 | 105 | test "inverse of one_to_many should be many_to_one" do 106 | assert_equal true, Domain::Relationship::Cardinality.new(0..1, 0..@n).inverse.many_to_one? 107 | end 108 | 109 | # Cardinality order ======================================================== 110 | test "cardinalities should be sorted in order of maniness" do 111 | card1 = Domain::Relationship::Cardinality.new(0..1, 1) 112 | card2 = Domain::Relationship::Cardinality.new(1, 1) 113 | card3 = Domain::Relationship::Cardinality.new(0..1, 1..3) 114 | card4 = Domain::Relationship::Cardinality.new(1, 1..2) 115 | card5 = Domain::Relationship::Cardinality.new(1, 1..@n) 116 | card6 = Domain::Relationship::Cardinality.new(1..5, 1..3) 117 | card7 = Domain::Relationship::Cardinality.new(1..2, 1..15) 118 | card8 = Domain::Relationship::Cardinality.new(1..15, 1..@n) 119 | card9 = Domain::Relationship::Cardinality.new(1..@n, 1..@n) 120 | assert_equal [card1, card2, card3, card4, card5, card6, card7, card8, card9], 121 | [card9, card5, card8, card2, card4, card7, card1, card6, card3].sort 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/unit/config_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 3 | 4 | class ConfigTest < ActiveSupport::TestCase 5 | 6 | test "load_config_gile should return blank hash when neither CURRENT_CONFIG_FILE nor USER_WIDE_CONFIG_FILE exist." do 7 | expected_hash = {} 8 | assert_equal expected_hash, RailsERD::Config.load 9 | end 10 | 11 | test "load_config_gile should return a hash from USER_WIDE_CONFIG_FILE when only USER_WIDE_CONFIG_FILE exists." do 12 | set_user_config_file_to("erdconfig.example") 13 | 14 | expected_hash = { 15 | attributes: [:content, :foreign_key, :inheritance, :false], 16 | disconnected: true, 17 | filename: "erd", 18 | filetype: :pdf, 19 | indirect: true, 20 | inheritance: false, 21 | markup: true, 22 | notation: :simple, 23 | orientation: "horizontal", 24 | polymorphism: false, 25 | warn: true, 26 | title: "sample title", 27 | exclude: [], 28 | only: [] 29 | } 30 | assert_equal expected_hash, RailsERD::Config.load 31 | end 32 | 33 | test "load_config_file should return a hash from USER_WIDE_CONFIG_FILE when only USER_WIDE_CONFIG_FILE exists." do 34 | set_user_config_file_to("erdconfig.exclude.example") 35 | 36 | expected_hash = { 37 | attributes: [:content, :foreign_key, :inheritance, :false], 38 | disconnected: true, 39 | filename: "erd", 40 | filetype: :pdf, 41 | indirect: true, 42 | inheritance: false, 43 | markup: true, 44 | notation: :simple, 45 | orientation: "horizontal", 46 | polymorphism: false, 47 | warn: true, 48 | title: "sample title", 49 | exclude: ['Book', 'Author'], 50 | only: [] 51 | } 52 | assert_equal expected_hash, RailsERD::Config.load 53 | end 54 | 55 | test "load_config_gile should return a hash from CURRENT_CONFIG_FILE when only CURRENT_CONFIG_FILE exists." do 56 | set_local_config_file_to("erdconfig.another_example") 57 | 58 | expected_hash = { 59 | :attributes => [:primary_key] 60 | } 61 | assert_equal expected_hash, RailsERD::Config.load 62 | end 63 | 64 | test "load_config_file should return a hash from the configured config file when a new config file is given as an argument" do 65 | set_local_config_file_to("erdconfig.another_example") 66 | 67 | expected_hash = { 68 | attributes: [:content, :foreign_key, :inheritance, :false], 69 | disconnected: true, 70 | filename: "erd", 71 | filetype: :pdf, 72 | indirect: true, 73 | inheritance: false, 74 | markup: true, 75 | notation: :simple, 76 | orientation: "horizontal", 77 | polymorphism: false, 78 | warn: true, 79 | title: "sample title", 80 | exclude: [], 81 | only: [] 82 | } 83 | 84 | assert_equal expected_hash, RailsERD::Config.load("test/support_files/erdconfig.example") 85 | end 86 | 87 | test "load_config_gile should return a hash from CURRENT_CONFIG_FILE overriding USER_WIDE_CONFIG_FILE when both of them exist." do 88 | set_user_config_file_to("erdconfig.example") 89 | set_local_config_file_to("erdconfig.another_example") 90 | 91 | expected_hash = { 92 | attributes: [:primary_key], 93 | disconnected: true, 94 | filename: "erd", 95 | filetype: :pdf, 96 | indirect: true, 97 | inheritance: false, 98 | markup: true, 99 | notation: :simple, 100 | orientation: "horizontal", 101 | polymorphism: false, 102 | warn: true, 103 | title: "sample title", 104 | exclude: [], 105 | only: [] 106 | } 107 | assert_equal expected_hash, RailsERD::Config.load 108 | end 109 | 110 | test "normalize_value should return symbols in an array when key is :attributes and value is a comma-joined string." do 111 | assert_equal [:content, :foreign_keys], normalize_value(:attributes, "content,foreign_keys") 112 | end 113 | 114 | test "normalize_value should return symbols in an array when key is :attributes and value is strings in an array." do 115 | assert_equal [:content, :primary_keys], normalize_value(:attributes, ["content", "primary_keys"]) 116 | end 117 | 118 | test "normalize_value should return hash with symbol keys when key is :fonts and value is a hash." do 119 | fonts_value = { "normal" => "Arial", "bold" => "Arial Bold", "italic" => "Arial Italic" } 120 | expected = {:normal => "Arial", :bold => "Arial Bold", :italic => "Arial Italic"} 121 | assert_equal expected, normalize_value(:fonts, fonts_value) 122 | end 123 | 124 | def normalize_value(key, value) 125 | RailsERD::Config.new.send(:normalize_value, key, value) 126 | end 127 | 128 | def set_user_config_file_to(config_file) 129 | RailsERD::Config.send :remove_const, :USER_WIDE_CONFIG_FILE 130 | RailsERD::Config.send :const_set, :USER_WIDE_CONFIG_FILE, 131 | File.expand_path("test/support_files/#{config_file}") 132 | end 133 | 134 | def set_local_config_file_to(config_file) 135 | RailsERD::Config.send :remove_const, :CURRENT_CONFIG_FILE 136 | RailsERD::Config.send :const_set, :CURRENT_CONFIG_FILE, 137 | File.expand_path("test/support_files/#{config_file}") 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/unit/mermaid_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 2 | require "rails_erd/diagram/mermaid" 3 | 4 | class MermaidTest < ActiveSupport::TestCase 5 | def setup 6 | RailsERD.options.filetype = :png 7 | RailsERD.options.warn = false 8 | end 9 | 10 | def teardown 11 | FileUtils.rm Dir["erd*.*"] rescue nil 12 | end 13 | 14 | def diagram(options = {}) 15 | @diagram ||= Diagram::Mermaid.new(Domain.generate(options), options).tap do |diagram| 16 | diagram.generate 17 | end 18 | end 19 | 20 | def find_dot_nodes(diagram) 21 | [].tap do |nodes| 22 | diagram.graph.each_node do |name, node| 23 | nodes << node 24 | end 25 | end 26 | end 27 | 28 | # Diagram properties ======================================================= 29 | test "file name should be mmd" do 30 | create_simple_domain 31 | begin 32 | assert_equal "erd.mmd", Diagram::Mermaid.create 33 | ensure 34 | FileUtils.rm "erd.mmd" rescue nil 35 | end 36 | end 37 | 38 | test "direction should be right to left" do 39 | create_simple_domain 40 | 41 | assert_equal "\tdirection RL", diagram.graph[1] 42 | end 43 | 44 | 45 | # # Diagram generation ======================================================= 46 | test "create should create output for domain with attributes" do 47 | create_model "Foo", :bar => :references, :column => :string do 48 | belongs_to :bar 49 | end 50 | 51 | create_model "Bar", :column => :string 52 | 53 | expected = [ 54 | "classDiagram", 55 | "\tdirection RL", 56 | "\tclass `Bar`", 57 | "\t`Bar` : +string column", 58 | "\tclass `Foo`", 59 | "\t`Foo` : +string column", 60 | "\t`Bar` --> `Foo`" 61 | ] 62 | 63 | assert_equal expected, diagram.graph 64 | end 65 | 66 | test "create should create output for domain without attributes" do 67 | create_simple_domain 68 | 69 | expected = [ 70 | "classDiagram", 71 | "\tdirection RL", 72 | "\tclass `Bar`", 73 | "\tclass `Beer`", 74 | "\t`Bar` --> `Beer`" 75 | ] 76 | 77 | assert_equal expected, diagram.graph 78 | end 79 | 80 | test "create should abort and complain if there are no connected models" do 81 | message = nil 82 | begin 83 | Diagram::Mermaid.create 84 | rescue => e 85 | message = e.message 86 | end 87 | assert_match(/No entities found/, message) 88 | end 89 | 90 | test "create should abort and complain if output directory does not exist" do 91 | message = nil 92 | 93 | begin 94 | create_simple_domain 95 | Diagram::Mermaid.create(:filename => "does_not_exist/foo") 96 | rescue => e 97 | message = e.message 98 | end 99 | 100 | assert_match(/Output directory 'does_not_exist' does not exist/, message) 101 | end 102 | 103 | test "generate should add attributes to entity" do 104 | RailsERD.options.markup = false 105 | create_model "Foo", :bar => :references do 106 | belongs_to :bar 107 | end 108 | create_model "Bar", :column => :string, :column_two => :boolean 109 | 110 | expected = [ 111 | "classDiagram", 112 | "\tdirection RL", 113 | "\tclass `Bar`", 114 | "\t`Bar` : +string column", 115 | "\t`Bar` : +boolean column_two", 116 | "\tclass `Foo`", 117 | "\t`Bar` --> `Foo`" 118 | ] 119 | 120 | assert_equal expected, diagram.graph 121 | end 122 | 123 | test "generate should not add any attributes if attributes is set to false" do 124 | create_model "Jar", :contents => :string 125 | create_model "Lid", :jar => :references do 126 | belongs_to :jar 127 | end 128 | 129 | expected = [ 130 | "classDiagram", 131 | "\tdirection RL", 132 | "\tclass `Jar`", 133 | "\tclass `Lid`", 134 | "\t`Jar` --> `Lid`" 135 | ] 136 | 137 | assert_equal expected, diagram(:attributes => false).graph 138 | end 139 | 140 | test "generate should create edge to polymorphic entity if polymorphism is true" do 141 | create_model "Cannon", :defensible => :references do 142 | belongs_to :defensible, :polymorphic => true 143 | end 144 | 145 | create_model "Stronghold" do 146 | has_many :cannons, :as => :defensible 147 | end 148 | 149 | create_model "Galleon" do 150 | has_many :cannons, :as => :defensible 151 | end 152 | 153 | expected = [ 154 | "classDiagram", 155 | "\tdirection RL", 156 | "\tclass `Cannon`", 157 | "\tclass `Defensible`", 158 | "\tclass `Galleon`", 159 | "\tclass `Stronghold`", 160 | "\t<> `Defensible`", 161 | "\t Defensible <|-- Galleon", 162 | "\t Defensible <|-- Stronghold", 163 | "\t`Defensible` --> `Cannon`", 164 | "\t`Galleon` --> `Cannon`", 165 | "\t`Stronghold` --> `Cannon`" 166 | ] 167 | 168 | assert_equal expected, diagram(:polymorphism => true).graph.uniq 169 | end 170 | 171 | test "generate should create edge to each child of polymorphic entity if polymorphism is false" do 172 | create_model "Cannon", :defensible => :references do 173 | belongs_to :defensible, :polymorphic => true 174 | end 175 | 176 | create_model "Stronghold" do 177 | has_many :cannons, :as => :defensible 178 | end 179 | 180 | create_model "Galleon" do 181 | has_many :cannons, :as => :defensible 182 | end 183 | 184 | expected = [ 185 | "classDiagram", 186 | "\tdirection RL", 187 | "\tclass `Cannon`", 188 | "\tclass `Galleon`", 189 | "\tclass `Stronghold`", 190 | "\t`Defensible` --> `Cannon`", 191 | "\t`Galleon` --> `Cannon`", 192 | "\t`Stronghold` --> `Cannon`" 193 | ] 194 | assert_equal expected, diagram.graph.uniq 195 | end 196 | 197 | test "generate should support one to many relationships" do 198 | create_one_to_many_assoc_domain 199 | 200 | expected = [ 201 | "classDiagram", 202 | "\tdirection RL", 203 | "\tclass `Many`", 204 | "\tclass `One`", 205 | "\t`One` --> `Many`" 206 | ] 207 | 208 | assert_equal expected, diagram.graph.uniq 209 | end 210 | 211 | test "generate should support one to many indirect relationships" do 212 | create_model "Foo" do 213 | has_many :bazs 214 | has_many :bars 215 | end 216 | 217 | create_model "Bar", :foo => :references do 218 | belongs_to :foo 219 | has_many :bazs, :through => :foo 220 | end 221 | 222 | create_model "Baz", :foo => :references do 223 | belongs_to :foo 224 | end 225 | 226 | expected = [ 227 | "classDiagram", 228 | "\tdirection RL", 229 | "\tclass `Bar`", 230 | "\tclass `Baz`", 231 | "\tclass `Foo`", 232 | "\t`Foo` --> `Baz`", 233 | "\t`Foo` --> `Bar`", 234 | "\t`Bar` ..> `Baz`" 235 | ] 236 | 237 | assert_equal expected, diagram.graph.uniq 238 | end 239 | 240 | test "generate should support many to many relationships" do 241 | create_many_to_many_assoc_domain 242 | 243 | expected = [ 244 | "classDiagram", 245 | "\tdirection RL", 246 | "\tclass `Many`", 247 | "\tclass `More`", 248 | "\t`Many` <--> `More`" 249 | ] 250 | 251 | assert_equal expected, diagram.graph.uniq 252 | end 253 | 254 | test "generate should support one to one relationships" do 255 | create_one_to_one_assoc_domain 256 | 257 | expected = [ 258 | "classDiagram", 259 | "\tdirection RL", 260 | "\tclass `One`", 261 | "\tclass `Other`", 262 | "\t`One` -- `Other`" 263 | ] 264 | 265 | assert_equal expected, diagram.graph.uniq 266 | end 267 | 268 | test "generate should support one to one recursive relationships" do 269 | create_model "Emperor" do 270 | belongs_to :predecessor, :class_name => "Emperor" 271 | has_one :successor, :class_name => "Emperor", :foreign_key => :predecessor_id 272 | end 273 | 274 | expected = [ 275 | "classDiagram", 276 | "\tdirection RL", 277 | "\tclass `Emperor`", 278 | "\t`Emperor` -- `Emperor`" 279 | ] 280 | 281 | assert_equal expected, diagram.graph.uniq 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /test/unit/rake_task_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 2 | require "rails_erd/diagram/graphviz" 3 | require "rails_erd/diagram/mermaid" 4 | 5 | class RakeTaskTest < ActiveSupport::TestCase 6 | include ActiveSupport::Testing::Isolation 7 | 8 | def setup 9 | require "rake" 10 | load "rails_erd/tasks.rake" 11 | 12 | RailsERD.options.filetype = :dot 13 | RailsERD.options.warn = false 14 | Rake.application.options.silent = true 15 | end 16 | 17 | def teardown 18 | FileUtils.rm "erd.dot" rescue nil 19 | end 20 | 21 | define_method :create_app do 22 | Object::Quux = Module.new 23 | Object::Quux::Application = Class.new 24 | Object::Rails = Struct.new(:application).new(Object::Quux::Application.new) 25 | 26 | Rails.class_eval do 27 | define_method :backtrace_cleaner do 28 | ActiveSupport::BacktraceCleaner.new.tap do |cleaner| 29 | cleaner.add_filter { |line| line.sub(File.dirname(__FILE__), "test/unit") } 30 | cleaner.add_silencer { |line| line !~ /^test\/unit/ } 31 | end 32 | end 33 | end 34 | end 35 | 36 | # Diagram generation ======================================================= 37 | test "generate task should create output based on domain model with graphviz by default" do 38 | create_simple_domain 39 | 40 | Diagram.any_instance.expects(:save) 41 | Rake::Task["erd:options"].execute 42 | Rake::Task["erd:generate"].execute 43 | end 44 | 45 | test "generate task should create output based on domain model with mermaid" do 46 | create_simple_domain 47 | 48 | ENV["generator"] = "mermaid" 49 | RailsERD::Diagram::Mermaid.any_instance.expects(:save) 50 | Rake::Task["erd:options"].execute 51 | Rake::Task["erd:generate"].execute 52 | end 53 | 54 | test "generate task should not create output if there are no connected models" do 55 | Rake::Task["erd:generate"].execute rescue nil 56 | assert !File.exist?("erd.dot") 57 | end 58 | 59 | test "generate task should eager load application environment" do 60 | eager_loaded, environment_loaded = nil 61 | create_app 62 | 63 | Rails.application.class_eval do 64 | define_method :eager_load! do 65 | eager_loaded = true 66 | end 67 | end 68 | 69 | Rake::Task.define_task :environment do 70 | environment_loaded = true 71 | end 72 | 73 | create_simple_domain 74 | 75 | Rake::Task["erd:generate"].invoke 76 | 77 | assert_equal [true, true], [eager_loaded, environment_loaded] 78 | end 79 | 80 | test "generate task should complain if active record is not loaded" do 81 | create_app 82 | 83 | Rails.application.class_eval do 84 | define_method :eager_load! do end 85 | end 86 | 87 | Rake::Task.define_task :environment 88 | Object.send :remove_const, :ActiveRecord 89 | message = nil 90 | 91 | begin 92 | Rake::Task["erd:generate"].invoke 93 | rescue => e 94 | message = e.message 95 | end 96 | assert_equal "Active Record was not loaded.", message 97 | end 98 | 99 | test "generate task should complain with simplified stack trace if application could not be loaded" do 100 | create_app 101 | l1, l2 = nil, nil 102 | Rails.application.class_eval do 103 | define_method :eager_load! do 104 | l1 = __LINE__ + 1 105 | raise "FooBar" 106 | end 107 | end 108 | Rake::Task.define_task :environment 109 | message = nil 110 | begin 111 | l2 = __LINE__ + 1 112 | Rake::Task["erd:generate"].invoke 113 | rescue => e 114 | message = e.message 115 | end 116 | assert_match(/#{Regexp.escape(<<-MSG.strip).gsub("xxx", ".*?")}/, message 117 | Loading models failed! 118 | Error occurred while loading application: FooBar (RuntimeError) 119 | test/unit/rake_task_test.rb:#{l1}:in `xxx' 120 | test/unit/rake_task_test.rb:#{l2}:in `xxx' 121 | MSG 122 | ) 123 | end 124 | 125 | test "generate task should reraise if application could not be loaded and trace option is enabled" do 126 | create_app 127 | Rails.application.class_eval do 128 | define_method :eager_load! do 129 | raise "FooBar" 130 | end 131 | end 132 | Rake::Task.define_task :environment 133 | message = nil 134 | begin 135 | old_stderr, $stderr = $stderr, StringIO.new 136 | Rake.application.options.trace = true 137 | Rake::Task["erd:generate"].invoke 138 | rescue => e 139 | message = e.message 140 | ensure 141 | $stderr = old_stderr 142 | end 143 | assert_equal "FooBar", message 144 | end 145 | 146 | # Option processing ======================================================== 147 | test "options task should ignore unknown command line options" do 148 | ENV["unknownoption"] = "value" 149 | Rake::Task["erd:options"].execute 150 | assert_nil RailsERD.options.unknownoption 151 | end 152 | 153 | test "options task should set known command line options" do 154 | ENV["filetype"] = "myfiletype" 155 | Rake::Task["erd:options"].execute 156 | assert_equal :myfiletype, RailsERD.options.filetype 157 | end 158 | 159 | test "options task should set known boolean command line options if false" do 160 | ENV["title"] = "false" 161 | Rake::Task["erd:options"].execute 162 | assert_equal false, RailsERD.options.title 163 | end 164 | 165 | test "options task should set known boolean command line options if true" do 166 | ENV["title"] = "true" 167 | Rake::Task["erd:options"].execute 168 | assert_equal true, RailsERD.options.title 169 | end 170 | 171 | test "options task should set known boolean command line options if no" do 172 | ENV["title"] = "no" 173 | Rake::Task["erd:options"].execute 174 | assert_equal false, RailsERD.options.title 175 | end 176 | 177 | test "options task should set known boolean command line options if yes" do 178 | ENV["title"] = "yes" 179 | Rake::Task["erd:options"].execute 180 | assert_equal true, RailsERD.options.title 181 | end 182 | 183 | test "options task should set known array command line options" do 184 | ENV["attributes"] = "content,timestamps" 185 | Rake::Task["erd:options"].execute 186 | assert_equal %w[content timestamps], RailsERD.options.attributes 187 | end 188 | 189 | test "options task should set known integer command line options when value is only digits" do 190 | ENV["only_recursion_depth"] = "2" 191 | Rake::Task["erd:options"].execute 192 | assert_equal 2, RailsERD.options.only_recursion_depth 193 | end 194 | 195 | test "options task sets known command line options as symbols when not boolean or numeric" do 196 | ENV["only_recursion_depth"] = "test" 197 | Rake::Task["erd:options"].execute 198 | assert_equal :test, RailsERD.options.only_recursion_depth 199 | end 200 | 201 | test "options task sets generator type" do 202 | Rake::Task["erd:options"].execute 203 | assert_equal :graphviz, RailsERD.options.generator 204 | 205 | ENV["generator"] = "mermaid" 206 | Rake::Task["erd:options"].execute 207 | assert_equal :mermaid, RailsERD.options.generator 208 | end 209 | 210 | test "options task should set single parameter to only as array xxx" do 211 | ENV["only"] = "model" 212 | Rake::Task["erd:options"].execute 213 | assert_equal ["model"], RailsERD.options.only 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /test/unit/specialization_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 2 | 3 | class SpecializationTest < ActiveSupport::TestCase 4 | # Specialization =========================================================== 5 | test "inspect should show source and destination" do 6 | create_specialization 7 | domain = Domain.generate 8 | assert_match %r{#}, 9 | Domain::Specialization.new(domain, domain.entity_by_name("Beverage"), domain.entity_by_name("Beer")).inspect 10 | end 11 | 12 | test "generalized should return source entity" do 13 | create_specialization 14 | domain = Domain.generate 15 | assert_equal domain.entity_by_name("Beverage"), 16 | Domain::Specialization.new(domain, domain.entity_by_name("Beverage"), domain.entity_by_name("Beer")).generalized 17 | end 18 | 19 | test "specialized should return destination entity" do 20 | create_specialization 21 | domain = Domain.generate 22 | assert_equal domain.entity_by_name("Beer"), 23 | Domain::Specialization.new(domain, domain.entity_by_name("Beverage"), domain.entity_by_name("Beer")).specialized 24 | end 25 | 26 | # Specialization properties ================================================ 27 | test "inheritance should be true for inheritance specializations" do 28 | create_specialization 29 | assert_equal [true], Domain.generate.specializations.map(&:inheritance?) 30 | end 31 | 32 | test "polymorphic should be false for inheritance specializations" do 33 | create_specialization 34 | assert_equal [false], Domain.generate.specializations.map(&:polymorphic?) 35 | end 36 | 37 | test "inheritance should be false for polymorphic specializations" do 38 | create_polymorphic_generalization 39 | assert_equal [false], Domain.generate.specializations.map(&:inheritance?) 40 | end 41 | 42 | test "polymorphic should be true for polymorphic specializations" do 43 | create_polymorphic_generalization 44 | assert_equal [true], Domain.generate.specializations.map(&:polymorphic?) 45 | end 46 | 47 | test "inheritance should be false for abstract specializations" do 48 | create_abstract_generalization 49 | assert_equal [false], Domain.generate.specializations.map(&:inheritance?) 50 | end 51 | 52 | test "polymorphic should be true for abstract specializations" do 53 | create_abstract_generalization 54 | assert_equal [true], Domain.generate.specializations.map(&:polymorphic?) 55 | end 56 | 57 | test "inheritance should be false for polymorphic specializations to specialized entities" do 58 | create_model "Cannon" 59 | create_model "Ship", :type => :string 60 | create_model "Galleon", Ship do 61 | has_many :cannons, :as => :defensible 62 | end 63 | domain = Domain.generate 64 | assert_equal false, domain.specializations.find { |s| 65 | s.generalized == domain.entity_by_name("Defensible") }.inheritance? 66 | end 67 | end 68 | --------------------------------------------------------------------------------