pferd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +12 -0
- data/lib/pferd/entity.rb +24 -0
- data/lib/pferd/entity_relationship.rb +6 -0
- data/lib/pferd/version.rb +5 -0
- data/lib/pferd.rb +37 -0
- data/lib/tasks/pferd.rake +79 -0
- data/sig/pferd.rbs +4 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4cfbec7f22022f6fde8257a419ae16a322a33b1a5a3b808066615c9e5a36c49a
|
4
|
+
data.tar.gz: 725ae61787aa419a0e97f5fa3b82e285cf116b440884f94fbcd72300c70a3d01
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0b39ddbc987ef910722df3b0514b3a66a1402c0e9cba2cd516a74ad4992b55cdcfe9a89868fa1a42b5453cc3fed97aaf408e547554ddfc546fd2ee1f87482094
|
7
|
+
data.tar.gz: 945a120e05fa4107328ea6bdc1d5629b6e975d3a1fe387b190535ca7e8794e9e1c7d00ffbcf28b05641bf4311df49131896c324060d40be952decd76c74d1a54
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Bodacious
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# Pferd
|
2
|
+
|
3
|
+
Pferd is a Ruby gem that generates an **Entity Relationship Diagram (ERD)** of your Ruby on Rails models, automatically **grouping models by their domain**. This is useful when you are in the process of modularising your codebase, and want to visualise various domain configurations without having to move code around.
|
4
|
+
|
5
|
+
The diagram (optionally) highlights **domain boundary violations**, giving you insights into possible cross-domain coupling issues.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'pferd'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
bundle install
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it directly using:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
gem install pferd
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
To generate an ERD, simply run:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
rake pferd:generate
|
33
|
+
```
|
34
|
+
|
35
|
+
## Defining Domains
|
36
|
+
|
37
|
+
Pferd groups your models by domain using the `@domain` YARD tag. To specify the domain of a model, add a YARD comment above the model class definition. For example:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# @domain payments
|
41
|
+
class Transaction < ApplicationRecord
|
42
|
+
# Model code
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
If a model does not have a `@domain` tag, it will be placed in the default domain (`Global` by default).
|
47
|
+
|
48
|
+
## Configuration
|
49
|
+
|
50
|
+
Pferd comes with configurable options that you can customise. Below is the default configuration:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Establish some default configs
|
54
|
+
Pferd.configure do |config|
|
55
|
+
# Classes without an explicit domain tag should be in this domain
|
56
|
+
config.default_domain_name = 'Global'
|
57
|
+
|
58
|
+
# Exclude these classes
|
59
|
+
config.ignored_classes = []
|
60
|
+
|
61
|
+
# Exclude classes nested in these modules
|
62
|
+
config.ignored_modules = ['ActiveStorage']
|
63
|
+
|
64
|
+
# Load models matching these glob-paths
|
65
|
+
config.model_dirs = ['app/models']
|
66
|
+
|
67
|
+
# The name of the generated output file
|
68
|
+
config.output_file_name = 'pferd.png'
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
### **How to customise:**
|
73
|
+
You can modify the configuration within an initializer (`config/initializers/pferd.rb`):
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
Pferd.configure do |config|
|
77
|
+
config.default_domain_name = 'Core'
|
78
|
+
config.ignored_classes = ['SomeTemporaryModel']
|
79
|
+
config.ignored_modules = ['ActiveStorage', 'ActionMailbox']
|
80
|
+
config.model_dirs = ['app/models', 'engines/*/app/models']
|
81
|
+
config.output_file_name = 'custom_erd.png'
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
## Domain Boundary Violations
|
86
|
+
|
87
|
+
Pferd will analyse your model associations and **highlight where models reference entities from another domain**. Violations will be clearly marked in the ERD using special edge styles (e.g., dashed or highlighted lines).
|
88
|
+
|
89
|
+
This feature helps you identify and address cases where models may be overstepping domain boundaries, promoting better separation of concerns within your codebase.
|
90
|
+
|
91
|
+
## Contributing
|
92
|
+
|
93
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/your-github/pferd). This project is intended to be a safe, welcoming space for collaboration.
|
94
|
+
|
95
|
+
## License
|
96
|
+
|
97
|
+
Pferd is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/pferd/entity.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'entity_relationship'
|
3
|
+
module Pferd
|
4
|
+
class Entity
|
5
|
+
attr_accessor :klass_name, :associations, :domain
|
6
|
+
alias name klass_name
|
7
|
+
|
8
|
+
def initialize(klass_name, associations, domain)
|
9
|
+
@klass_name = klass_name
|
10
|
+
@associations = Set.new
|
11
|
+
@domain = domain
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_relationship(name: , domain: )
|
15
|
+
boundary_violation = self.domain != domain
|
16
|
+
associations.add(
|
17
|
+
EntityRelationship.new(name:, domain:, boundary_violation: boundary_violation)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
def to_h
|
21
|
+
{ entity: klass_name, domain: domain, associations: associations.map(&:to_h) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/pferd.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "pferd/version"
|
4
|
+
require_relative "pferd/entity"
|
5
|
+
require "ostruct"
|
6
|
+
module Pferd
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# just use an OpenStruct for flexible config until we know what configs we need
|
12
|
+
def configuration
|
13
|
+
@configuration ||= OpenStruct.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def configure
|
17
|
+
yield(configuration)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Establish some default configs
|
21
|
+
configure do |config|
|
22
|
+
# Classes without an explicit domain tag should be in this domain
|
23
|
+
config.default_domain_name = "Global"
|
24
|
+
# Exclude these classes
|
25
|
+
config.ignored_classes = []
|
26
|
+
# Exclude classes nested in these modules
|
27
|
+
config.ignored_modules = ["ActiveStorage"]
|
28
|
+
# Load models from these directories
|
29
|
+
config.model_dirs = ["app/models"]
|
30
|
+
# The name of the generated output file
|
31
|
+
config.output_file_name = "pferd.png"
|
32
|
+
# Highlight boundary violations
|
33
|
+
config.highlight_boundary_violations = true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
# Load this last so that the Rake file has access to the above config.
|
37
|
+
load "pferd/lib/tasks/pferd.rake"
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "yard"
|
5
|
+
require "pferd"
|
6
|
+
require "graphviz"
|
7
|
+
|
8
|
+
namespace :pferd do
|
9
|
+
# Load the models specified in config and generate their Docs
|
10
|
+
# (this is required to get the domain tag from the doc metadata)
|
11
|
+
YARD::Rake::YardocTask.new do |t|
|
12
|
+
t.files = Pferd.configuration.model_dirs.map { |dir| File.join(dir, "**", "*.rb") }
|
13
|
+
t.options = ["--tag", 'domain:"App domain"']
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Generate an ERD that shows models grouped by domain"
|
17
|
+
task draw_relationships: %i[environment pferd:yard] do
|
18
|
+
# Load the Yard registry
|
19
|
+
YARD::Registry.load!
|
20
|
+
MODEL_DOMAINS = Hash.new(Pferd.configuration.default_domain_name)
|
21
|
+
|
22
|
+
# Populate MODEL_DOMAINS with model names and their domains
|
23
|
+
YARD::Registry.all(:class).find_each do |klass_info|
|
24
|
+
domain_tag = klass_info.tags.find { |tag| tag.tag_name == "domain" }
|
25
|
+
MODEL_DOMAINS[klass_info.name.to_s] = domain_tag.text if domain_tag
|
26
|
+
end
|
27
|
+
|
28
|
+
# Load all models
|
29
|
+
Rails.application.eager_load!
|
30
|
+
|
31
|
+
entities = ApplicationRecord.descendants.map do |model|
|
32
|
+
next unless MODEL_DOMAINS.key?(model.name)
|
33
|
+
|
34
|
+
entity = Pferd::Entity.new(model.name, Set.new, MODEL_DOMAINS[model.name])
|
35
|
+
associations = (model.reflect_on_all_associations(:has_many) |
|
36
|
+
model.reflect_on_all_associations(:has_one))
|
37
|
+
associations.each do |assoc|
|
38
|
+
next if Pferd.configuration.ignored_classes.include?(assoc.klass.name)
|
39
|
+
next if Pferd.configuration.ignored_modules.any? do |module_name|
|
40
|
+
assoc.klass.name.start_with?(module_name)
|
41
|
+
end
|
42
|
+
# TODO: Figure out what to do with polymorphic relations
|
43
|
+
next if assoc.polymorphic?
|
44
|
+
|
45
|
+
entity.add_relationship(name: assoc.klass.name, domain: MODEL_DOMAINS[assoc.klass.name])
|
46
|
+
end.compact
|
47
|
+
entity
|
48
|
+
end.compact
|
49
|
+
|
50
|
+
g = GraphViz.new(:G, type: :digraph)
|
51
|
+
node_map = Hash.new { |hash, key| hash[key] = g.add_graph("cluster_#{key}", label: key) }
|
52
|
+
|
53
|
+
# Create subgraphs and nodes by domains
|
54
|
+
# Note: This has to be done as a complete loop before the next step, because we'll
|
55
|
+
# get an exception if we try to define edges before all of the nodes are defined.
|
56
|
+
entities.each do |entity|
|
57
|
+
subgraph = node_map[entity.domain]
|
58
|
+
subgraph.add_nodes(entity.name)
|
59
|
+
end
|
60
|
+
##
|
61
|
+
# See above comment
|
62
|
+
# rubocop:disable Style/CombinableLoops
|
63
|
+
entities.each do |entity|
|
64
|
+
entity.associations.each do |relationship|
|
65
|
+
color = if Pferd.configuration.highlight_boundary_violations && relationship.boundary_violation
|
66
|
+
"red"
|
67
|
+
else
|
68
|
+
"black"
|
69
|
+
end
|
70
|
+
g.add_edges(entity.name, relationship.name, color: color, style: "dashed")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# rubocop:enable Style/CombinableLoops
|
74
|
+
|
75
|
+
# Generate output as PNG file
|
76
|
+
g.output(png: Pferd.configuration.output_file_name)
|
77
|
+
end
|
78
|
+
task default: :draw_relationships
|
79
|
+
end
|
data/sig/pferd.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pferd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bodacious
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-03 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: ruby-graphviz
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: yard
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
description: Generate domain-based ERDs for Rails. Useful for modelling potential
|
41
|
+
conformations before committing to them
|
42
|
+
email:
|
43
|
+
- gavin@gavinmorrice.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- ".rubocop.yml"
|
51
|
+
- CHANGELOG.md
|
52
|
+
- LICENSE.txt
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- lib/pferd.rb
|
56
|
+
- lib/pferd/entity.rb
|
57
|
+
- lib/pferd/entity_relationship.rb
|
58
|
+
- lib/pferd/version.rb
|
59
|
+
- lib/tasks/pferd.rake
|
60
|
+
- sig/pferd.rbs
|
61
|
+
homepage: https://github.com/bodacious/pferd
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata:
|
65
|
+
homepage_uri: https://github.com/bodacious/pferd
|
66
|
+
source_code_uri: https://github.com/bodacious/pferd
|
67
|
+
changelog_uri: https://github.com/bodacious/pferd
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.0.0
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubygems_version: 3.6.3
|
83
|
+
specification_version: 4
|
84
|
+
summary: Generate domain-based ERDs for Rails
|
85
|
+
test_files: []
|