stellwerk 0.0.1 → 0.0.2
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 +4 -4
- data/AGENTS.md +9 -0
- data/CHANGELOG.md +7 -1
- data/IMPLEMENTATION.md +6 -1
- data/README.md +19 -34
- data/lib/stellwerk/commands/check.rb +58 -0
- data/lib/stellwerk/config.rb +28 -0
- data/lib/stellwerk/printer.rb +26 -0
- data/lib/stellwerk/railtie.rb +9 -0
- data/lib/stellwerk/rules/layers.rb +58 -0
- data/lib/stellwerk/version.rb +1 -1
- data/lib/stellwerk/violation.rb +3 -0
- data/lib/stellwerk.rb +4 -1
- data/lib/tasks/stellwerk.rake +13 -0
- metadata +39 -6
- data/exe/stellwerk +0 -3
- data/lib/stellwerk/command.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5bc717030b6f075aebb0615a37ee45cf32aafe5cc7d5e9e7d5bbf50350f6cfa
|
|
4
|
+
data.tar.gz: 6d03317e06b5d44b174b734424a72543943f5f62a39ff4d3cba9a6b1a64a9b0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a87d9aa67acfa4a3c5df1119d6c8aaa1e1f4ea98c8a923445cd970c1b783e3a25db0bae5f5b12950d61fc975ed75c6a1423ecf62ff5b31c3a81d16e9ce76428e
|
|
7
|
+
data.tar.gz: 11a17aa329b2194b587e66d5e316e978c026dd2114795ce7dda1c95434e9bb037b981972625db420679de2df5eff66f6ccc26186891fb0b48d9facecdf7857b9
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Keep it simple
|
|
4
|
+
|
|
5
|
+
We're in pre-alpha. Don't overengineer, don't overdocument, don't put too much emphasis on UX. We want to prove the basics first.
|
|
6
|
+
|
|
7
|
+
## Usage Expectations
|
|
8
|
+
|
|
9
|
+
This gem can only be used with Rails applications for now. In theory we can support any Ruby codebase using zeitwerk, but that's not a priority at this point.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.0.2] - 2026-02-15
|
|
4
|
+
|
|
5
|
+
- MVP: Basic architecture enforcement with layered architecture support
|
|
6
|
+
- **breaking:** invoke stellwerk via rake task (`rails stellwerk:check`), the executable is removed
|
|
7
|
+
- pull actual autoloaders from application, or use faked autoloaders to omit app startup via `rails stellwerk:check_simple`
|
|
8
|
+
|
|
3
9
|
## [0.0.1] - 2025-12-10
|
|
4
10
|
|
|
5
|
-
- Initial release
|
|
11
|
+
- Initial release without any functionality. Essentially, name squatting.
|
data/IMPLEMENTATION.md
CHANGED
|
@@ -63,8 +63,13 @@ Considerations:
|
|
|
63
63
|
jobs should probably live in the business logic layer
|
|
64
64
|
- what about lib?
|
|
65
65
|
|
|
66
|
-
##
|
|
66
|
+
## Todos
|
|
67
|
+
|
|
68
|
+
- [ ] Implement VSCode integration (lots of things TBD)
|
|
69
|
+
|
|
70
|
+
## Ideas
|
|
67
71
|
|
|
68
72
|
- Shortcuts
|
|
69
73
|
- for CLI command, use a CLI library instead of hand-rolling it
|
|
70
74
|
- probably, `optparse` built into the stdlib
|
|
75
|
+
- Support non-rails applications using Zeitwerk, probably by adding an executable
|
data/README.md
CHANGED
|
@@ -1,62 +1,47 @@
|
|
|
1
1
|
# Stellwerk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Stellwerk is a Ruby gem that helps you enforce architectural rules in your Ruby on Rails application. It analyzes your codebase and identifies violations of architectural constraints you specify.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Name
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The name "Stellwerk" is derived from the German word for "signal station" in a railway context. It keeps your Rails on track!
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Installation
|
|
10
10
|
|
|
11
11
|
Install the gem and add to the application's Gemfile by executing:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
bundle add
|
|
14
|
+
bundle add stellwerk
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
gem install
|
|
20
|
+
gem install stellwerk
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Development
|
|
28
|
-
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
-
|
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
-
|
|
33
|
-
## Publishing to RubyGems
|
|
25
|
+
Run the check task in your Rails app:
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
gem build stellwerk.gemspec
|
|
40
|
-
```
|
|
27
|
+
```bash
|
|
28
|
+
bin/rails stellwerk:check
|
|
29
|
+
```
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
3. Sign in to RubyGems (only needed once):
|
|
31
|
+
Run a simpler check without booting `:environment` (uses statically defined Zeitwerk loaders for `app/*` and `lib`) and will probably miss stuff in more complex Rails apps:
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
```bash
|
|
34
|
+
bin/rails stellwerk:check_simple
|
|
35
|
+
```
|
|
48
36
|
|
|
49
|
-
|
|
37
|
+
## Development
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
gem push stellwerk-<version>.gem
|
|
53
|
-
```
|
|
39
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
54
40
|
|
|
55
|
-
|
|
41
|
+
## Publishing to RubyGems
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```
|
|
43
|
+
1. Update the version in [`lib/stellwerk/version.rb`](lib/stellwerk/version.rb). Push / merge to main.
|
|
44
|
+
2. `rake release`
|
|
60
45
|
|
|
61
46
|
## Contributing
|
|
62
47
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "reference_extractor"
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "parallel"
|
|
5
|
+
|
|
6
|
+
require "stellwerk/config"
|
|
7
|
+
require "stellwerk/printer"
|
|
8
|
+
|
|
9
|
+
module Stellwerk
|
|
10
|
+
module Commands
|
|
11
|
+
class Check
|
|
12
|
+
def initialize(root_path, autoloaders: nil)
|
|
13
|
+
@root_path = Pathname.new(root_path)
|
|
14
|
+
@autoloaders = autoloaders || fake_autoloaders
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
puts "running check..."
|
|
19
|
+
config = Stellwerk::Config.new(@root_path.join("stellwerk.yml"))
|
|
20
|
+
|
|
21
|
+
# build graph using reference_extractor
|
|
22
|
+
extractor = ReferenceExtractor::Extractor.new(
|
|
23
|
+
autoloaders: @autoloaders,
|
|
24
|
+
root_path: @root_path
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
all_files = @root_path.find
|
|
28
|
+
.select { |path| path.to_s.end_with?(".rb") }
|
|
29
|
+
.reject { |path| path.relative_path_from(@root_path).to_s.start_with?("db/") }
|
|
30
|
+
puts "collected #{all_files.length} files"
|
|
31
|
+
|
|
32
|
+
before = Time.now
|
|
33
|
+
edgelist = Parallel.flat_map(all_files, in_processes: Parallel.processor_count - 1) do |file|
|
|
34
|
+
extractor.references_from_file(file)
|
|
35
|
+
end
|
|
36
|
+
puts "extracted #{edgelist.length} references in #{(Time.now - before).round(2)} seconds"
|
|
37
|
+
|
|
38
|
+
# check rules against graph, collect violations
|
|
39
|
+
puts "checking rules..."
|
|
40
|
+
violations = config.rules.flat_map do |rule|
|
|
41
|
+
rule.find_violations(edgelist)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Stellwerk::Printer.new(violations).print
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def fake_autoloaders
|
|
50
|
+
loader = Zeitwerk::Loader.new
|
|
51
|
+
@root_path.join("app").each_child { |child| loader.push_dir(child) }
|
|
52
|
+
loader.push_dir(@root_path.join("lib"))
|
|
53
|
+
loader.setup
|
|
54
|
+
[loader]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "active_support/core_ext/string"
|
|
2
|
+
|
|
3
|
+
module Stellwerk
|
|
4
|
+
class Config
|
|
5
|
+
class UnknownRule < StandardError; end
|
|
6
|
+
|
|
7
|
+
attr_reader :rules
|
|
8
|
+
|
|
9
|
+
def initialize(filepath)
|
|
10
|
+
config = YAML.load_file(filepath)
|
|
11
|
+
|
|
12
|
+
@rules = initialize_rules(config["rules"])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize_rules(rules_config)
|
|
16
|
+
rules_config.map do |rule_name, rule_config|
|
|
17
|
+
begin
|
|
18
|
+
require "stellwerk/rules/#{rule_name}"
|
|
19
|
+
rule_class = ("Stellwerk::Rules::" + rule_name.camelize).constantize
|
|
20
|
+
rescue LoadError, NameError
|
|
21
|
+
raise(UnknownRule, "Unknown rule: #{rule_name}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
rule_class.new(rule_config)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Stellwerk
|
|
2
|
+
class Printer
|
|
3
|
+
def initialize(violations)
|
|
4
|
+
@violations = violations
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def print
|
|
8
|
+
puts "\nFound #{@violations.length} violations."
|
|
9
|
+
|
|
10
|
+
@violations.group_by { |violation| violation.rule }.each do |rule, violations|
|
|
11
|
+
puts "#{rule.name.titleize} rule violations:"
|
|
12
|
+
violations.each do |violation|
|
|
13
|
+
puts " " + format_reference(violation.reference)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def format_reference(reference)
|
|
21
|
+
source = "#{reference.relative_path}:#{reference.source_location.line}"
|
|
22
|
+
target = "#{reference.constant.name}, defined in #{reference.constant.location}"
|
|
23
|
+
"#{source} refers to #{target}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
require "stellwerk/violation"
|
|
4
|
+
|
|
5
|
+
module Stellwerk
|
|
6
|
+
module Rules
|
|
7
|
+
class Layers
|
|
8
|
+
class InvalidLayersSpec < RuntimeError; end
|
|
9
|
+
|
|
10
|
+
def initialize(rule_spec)
|
|
11
|
+
@layers = rule_spec.map { |layer| Array(layer).map { |path_string| Pathname.new(path_string) } }
|
|
12
|
+
validate_layers!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find_violations(reference_graph)
|
|
16
|
+
relevant_graph = filter_graph(reference_graph)
|
|
17
|
+
|
|
18
|
+
relevant_graph.map do |reference|
|
|
19
|
+
# TO DO: violations should include some rule specific violation context
|
|
20
|
+
# e.g. from layer and to layer
|
|
21
|
+
if layer_index(reference.relative_path) > layer_index(reference.constant.location)
|
|
22
|
+
Violation.new(:layers, reference)
|
|
23
|
+
end
|
|
24
|
+
end.compact
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter_graph(reference_graph)
|
|
28
|
+
reference_graph.select do |reference|
|
|
29
|
+
all_components.any? { |component| path_in_component?(reference.relative_path, component) } &&
|
|
30
|
+
all_components.any? { |component| path_in_component?(reference.constant.location, component) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def all_components
|
|
35
|
+
@layers.flatten
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def path_in_component?(path, component)
|
|
39
|
+
# TO DO: Find out whether we can use Pathname methods to determine whether one path contains another
|
|
40
|
+
component_dirname = component.to_s
|
|
41
|
+
component_dirname += "/" unless component.to_s.end_with?("/")
|
|
42
|
+
path.to_s.start_with?(component_dirname)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def layer_index(file_path)
|
|
46
|
+
@layers.index do |components|
|
|
47
|
+
components.any? { |component| path_in_component?(file_path, component) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_layers!
|
|
52
|
+
raise InvalidLayersSpec, "Overlapping layers" if all_components.count != all_components.uniq.count
|
|
53
|
+
raise InvalidLayersSpec, "No layers exist" if @layers.empty?
|
|
54
|
+
raise InvalidLayersSpec, "Empty layers exist" if @layers.any?(&:empty?)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/stellwerk/version.rb
CHANGED
data/lib/stellwerk.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "stellwerk/version"
|
|
4
|
+
require_relative "stellwerk/commands/check"
|
|
5
|
+
|
|
6
|
+
require "rails/railtie"
|
|
7
|
+
require_relative "stellwerk/railtie"
|
|
4
8
|
|
|
5
9
|
module Stellwerk
|
|
6
10
|
class Error < StandardError; end
|
|
7
|
-
# Your code goes here...
|
|
8
11
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :stellwerk do
|
|
4
|
+
task check: :environment do
|
|
5
|
+
autoloaders = [Rails.autoloaders.main, Rails.autoloaders.once].compact
|
|
6
|
+
|
|
7
|
+
Stellwerk::Commands::Check.new(Rails.root, autoloaders: autoloaders).run
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
task :check_simple do
|
|
11
|
+
Stellwerk::Commands::Check.new(Dir.pwd).run
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stellwerk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Theus
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -37,24 +37,57 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: parallel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: railties
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
40
68
|
email:
|
|
41
69
|
- philip@simplexity.quest
|
|
42
|
-
executables:
|
|
43
|
-
- stellwerk
|
|
70
|
+
executables: []
|
|
44
71
|
extensions: []
|
|
45
72
|
extra_rdoc_files: []
|
|
46
73
|
files:
|
|
47
74
|
- ".tool-versions"
|
|
75
|
+
- AGENTS.md
|
|
48
76
|
- CHANGELOG.md
|
|
49
77
|
- CODE_OF_CONDUCT.md
|
|
50
78
|
- IMPLEMENTATION.md
|
|
51
79
|
- LICENSE.txt
|
|
52
80
|
- README.md
|
|
53
81
|
- Rakefile
|
|
54
|
-
- exe/stellwerk
|
|
55
82
|
- lib/stellwerk.rb
|
|
56
|
-
- lib/stellwerk/
|
|
83
|
+
- lib/stellwerk/commands/check.rb
|
|
84
|
+
- lib/stellwerk/config.rb
|
|
85
|
+
- lib/stellwerk/printer.rb
|
|
86
|
+
- lib/stellwerk/railtie.rb
|
|
87
|
+
- lib/stellwerk/rules/layers.rb
|
|
57
88
|
- lib/stellwerk/version.rb
|
|
89
|
+
- lib/stellwerk/violation.rb
|
|
90
|
+
- lib/tasks/stellwerk.rake
|
|
58
91
|
homepage: https://github.com/exterm/stellwerk
|
|
59
92
|
licenses:
|
|
60
93
|
- MIT
|
data/exe/stellwerk
DELETED
data/lib/stellwerk/command.rb
DELETED