snapbot 0.1.3 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -0
- data/README.md +28 -1
- data/Rakefile +3 -1
- data/Steepfile +43 -0
- data/docs/img/models-with-lets.svg +96 -0
- data/docs/img/models.svg +94 -0
- data/lib/snapbot/diagram/dot_generator.rb +13 -10
- data/lib/snapbot/diagram.rb +1 -0
- data/lib/snapbot/{diagram/dot_generator → reflector}/relationship.rb +1 -1
- data/lib/snapbot/reflector.rb +14 -2
- data/lib/snapbot/version.rb +1 -1
- data/lib/tasks/steep.rake +8 -0
- data/sig/lib/snapbot/diagram/dot_generator.rbs +25 -0
- data/sig/lib/snapbot/diagram/renderer.rbs +19 -0
- data/sig/lib/snapbot/diagram.rbs +9 -0
- data/sig/lib/snapbot/reflector/relationship.rbs +14 -0
- data/sig/lib/snapbot/reflector.rbs +38 -0
- data/sig/lib/snapbot/rspec/lets.rbs +25 -0
- data/snapbot.gemspec +2 -0
- metadata +41 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4bd122996f917b0e18388a91aaeba183028d54a6b5e2fc58c05559f3a5103b6a
|
4
|
+
data.tar.gz: 15ce511a21b7d1dd96553a45959dce0e9ebfb9ad9ba2624d369af615742ec80b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34c2e342537c4085fcef8e9ae08e2cde45d8b200e8f7d4e3f369f745ce41ddae3ac5b36a8735c7d568c37427038107e061b9525372f8cc6c9be21ab2d4ad6f20
|
7
|
+
data.tar.gz: 4fbe87ef32e963c62eb44fe5f54e26901f7dd33e0c1ca048d962a066a0bb9de2abb5516ccc24b20c84fe8e1f86c435b27c04d2bb85b2ddf30b06efd367d30dba
|
data/.rubocop.yml
CHANGED
@@ -1,12 +1,30 @@
|
|
1
1
|
AllCops:
|
2
2
|
TargetRubyVersion: 2.6
|
3
3
|
SuggestExtensions: false
|
4
|
+
NewCops: enable
|
5
|
+
Exclude:
|
6
|
+
- Steepfile
|
7
|
+
- vendor/bundle/**/*
|
8
|
+
|
9
|
+
Gemspec/RequireMFA:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/AbcSize:
|
13
|
+
Exclude:
|
14
|
+
- spec/support/fixture_database.rb
|
4
15
|
|
5
16
|
Metrics/BlockLength:
|
6
17
|
Exclude:
|
7
18
|
- spec/**/*_spec.rb
|
8
19
|
- snapbot.gemspec
|
9
20
|
|
21
|
+
Metrics/MethodLength:
|
22
|
+
Exclude:
|
23
|
+
- spec/support/fixture_database.rb
|
24
|
+
|
25
|
+
Style/DoubleNegation:
|
26
|
+
Enabled: false
|
27
|
+
|
10
28
|
Style/StderrPuts:
|
11
29
|
Exclude:
|
12
30
|
- lib/snapbot/diagram/renderer.rb
|
data/README.md
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
# Snapbot
|
2
2
|
|
3
|
+
![example workflow](https://github.com/rgarner/snapbot/actions/workflows/main.yml/badge.svg)
|
4
|
+
|
3
5
|
Snapbot generates little diagrams via `save_and_open_diagram` for you to visualise the small constellations of
|
4
6
|
ActiveRecord objects that you find in feature and integration tests. These are most often made by
|
5
7
|
[FactoryBot](https://github.com/thoughtbot/factory_bot) or some other fixture-handling method, but this gem has no
|
6
8
|
opinions on those (beyond namechecking).
|
9
|
+
|
10
|
+
![example](docs/img/models.svg)
|
7
11
|
|
8
12
|
## Installation
|
9
13
|
|
10
|
-
|
14
|
+
Snapbot requires [Graphviz](https://graphviz.org/download/#executable-packages), and cannot function without it.
|
15
|
+
Install this first, then add the gem to your project's `:test` group in the gemfile:
|
11
16
|
|
12
17
|
```ruby
|
13
18
|
group :test do
|
@@ -30,6 +35,28 @@ Use:
|
|
30
35
|
save_and_open_diagram
|
31
36
|
```
|
32
37
|
|
38
|
+
## RSpec integration
|
39
|
+
|
40
|
+
Sometimes it's useful to see the models you created as preconditions vs the ones your code has created in response to
|
41
|
+
those. If you use RSpec's [`let` construct](https://relishapp.com/rspec/rspec-core/v/3-11/docs/helper-methods/let-and-let)
|
42
|
+
then Snapbot will match these automatically for you and annotate the diagram. For example:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
include Snapbot::Diagram
|
46
|
+
describe "the categorised post creator" do
|
47
|
+
let(:blog) { create :blog }
|
48
|
+
let(:author) { create :author }
|
49
|
+
|
50
|
+
it "creates posts automatically, categorised" do
|
51
|
+
CategorisedPostCreator.new(blog, author).run
|
52
|
+
save_and_open_diagram
|
53
|
+
expect(Post.count).to eql(2)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
![annotated diagram](docs/img/models-with-lets.svg)
|
59
|
+
|
33
60
|
## Why?
|
34
61
|
|
35
62
|
Sometimes, you need to create a few ActiveRecord objects for your test suite. Sometimes, there will be a little cluster
|
data/Rakefile
CHANGED
@@ -3,10 +3,12 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
5
|
|
6
|
+
Dir["lib/tasks/*.rake"].each { |file| import file }
|
7
|
+
|
6
8
|
RSpec::Core::RakeTask.new(:spec)
|
7
9
|
|
8
10
|
require "rubocop/rake_task"
|
9
11
|
|
10
12
|
RuboCop::RakeTask.new
|
11
13
|
|
12
|
-
task default: %i[spec rubocop]
|
14
|
+
task default: %i[spec rubocop steep:check]
|
data/Steepfile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
D = Steep::Diagnostic
|
2
|
+
|
3
|
+
target :lib do
|
4
|
+
signature "sig"
|
5
|
+
|
6
|
+
check "lib" # Directory name
|
7
|
+
# check "Gemfile" # File name
|
8
|
+
# check "app/models/**/*.rb" # Glob
|
9
|
+
# ignore "lib/templates/*.rb"
|
10
|
+
|
11
|
+
library "set"
|
12
|
+
# library "rspec"
|
13
|
+
# library "strong_json" # Gems
|
14
|
+
|
15
|
+
# configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
|
16
|
+
configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
|
17
|
+
configure_code_diagnostics do |hash| # You can setup everything yourself
|
18
|
+
# lib/snapbot/reflector.rb:73:35: [error] Unsupported block params pattern, probably masgn?
|
19
|
+
# │ Diagnostic ID: Ruby::UnsupportedSyntax
|
20
|
+
# │
|
21
|
+
# └ hash.each_with_object([]) do |(key, value), array|
|
22
|
+
# ~~~~~~~~~~~~~~~~~~~~~
|
23
|
+
# This disables that ^^
|
24
|
+
hash[D::Ruby::UnsupportedSyntax] = :information
|
25
|
+
|
26
|
+
# lib/snapbot/diagram/renderer.rb:21:58: [error] The method cannot be called with a block
|
27
|
+
# │ Diagnostic ID: Ruby::UnexpectedBlockGiven
|
28
|
+
# │
|
29
|
+
# └ IO.popen("dot -Tsvg -o #{OUTPUT_FILENAME}", "w+") do |pipe|
|
30
|
+
# ~~~~~~~~~
|
31
|
+
#
|
32
|
+
# This disables that ^^ but probably a bit too much. Can we restrict to renderer.rb?
|
33
|
+
hash[D::Ruby::UnexpectedBlockGiven] = :information
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# target :test do
|
38
|
+
# signature "sig", "sig-private"
|
39
|
+
#
|
40
|
+
# check "test"
|
41
|
+
#
|
42
|
+
# # library "pathname", "set" # Standard libraries
|
43
|
+
# end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
3
|
+
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
4
|
+
<!-- Generated by graphviz version 3.0.0 (20220226.1711)
|
5
|
+
-->
|
6
|
+
<!-- Title: g Pages: 1 -->
|
7
|
+
<svg width="322pt" height="247pt"
|
8
|
+
viewBox="0.00 0.00 322.10 246.60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
9
|
+
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 217.8)">
|
10
|
+
<title>g</title>
|
11
|
+
<polygon fill="white" stroke="transparent" points="-28.8,28.8 -28.8,-217.8 293.3,-217.8 293.3,28.8 -28.8,28.8"/>
|
12
|
+
<!-- Category#1 -->
|
13
|
+
<g id="node1" class="node">
|
14
|
+
<title>Category#1</title>
|
15
|
+
<path fill="none" stroke="black" d="M12,-149.5C12,-149.5 57,-149.5 57,-149.5 63,-149.5 69,-155.5 69,-161.5 69,-161.5 69,-173.5 69,-173.5 69,-179.5 63,-185.5 57,-185.5 57,-185.5 12,-185.5 12,-185.5 6,-185.5 0,-179.5 0,-173.5 0,-173.5 0,-161.5 0,-161.5 0,-155.5 6,-149.5 12,-149.5"/>
|
16
|
+
<text text-anchor="start" x="9.5" y="-164.5" font-family="ArialMT" font-size="10.00">Category#1</text>
|
17
|
+
</g>
|
18
|
+
<!-- Post#1 -->
|
19
|
+
<g id="node4" class="node">
|
20
|
+
<title>Post#1</title>
|
21
|
+
<path fill="none" stroke="black" d="M77.5,-73.5C77.5,-73.5 107.5,-73.5 107.5,-73.5 113.5,-73.5 119.5,-79.5 119.5,-85.5 119.5,-85.5 119.5,-97.5 119.5,-97.5 119.5,-103.5 113.5,-109.5 107.5,-109.5 107.5,-109.5 77.5,-109.5 77.5,-109.5 71.5,-109.5 65.5,-103.5 65.5,-97.5 65.5,-97.5 65.5,-85.5 65.5,-85.5 65.5,-79.5 71.5,-73.5 77.5,-73.5"/>
|
22
|
+
<text text-anchor="start" x="77.5" y="-88.5" font-family="ArialMT" font-size="10.00">Post#1</text>
|
23
|
+
</g>
|
24
|
+
<!-- Category#1->Post#1 -->
|
25
|
+
<g id="edge1" class="edge">
|
26
|
+
<title>Category#1->Post#1</title>
|
27
|
+
<path fill="none" stroke="black" d="M53.51,-142.25C59.92,-134.07 67.07,-124.94 73.48,-116.76"/>
|
28
|
+
<polygon fill="black" stroke="black" points="51.02,-140.31 47.95,-149.34 55.98,-144.2 51.02,-140.31"/>
|
29
|
+
<polygon fill="black" stroke="black" points="76.05,-118.6 79.12,-109.57 71.09,-114.71 76.05,-118.6"/>
|
30
|
+
</g>
|
31
|
+
<!-- Post#2 -->
|
32
|
+
<g id="node5" class="node">
|
33
|
+
<title>Post#2</title>
|
34
|
+
<path fill="none" stroke="black" d="M160.5,-73.5C160.5,-73.5 190.5,-73.5 190.5,-73.5 196.5,-73.5 202.5,-79.5 202.5,-85.5 202.5,-85.5 202.5,-97.5 202.5,-97.5 202.5,-103.5 196.5,-109.5 190.5,-109.5 190.5,-109.5 160.5,-109.5 160.5,-109.5 154.5,-109.5 148.5,-103.5 148.5,-97.5 148.5,-97.5 148.5,-85.5 148.5,-85.5 148.5,-79.5 154.5,-73.5 160.5,-73.5"/>
|
35
|
+
<text text-anchor="start" x="160.5" y="-88.5" font-family="ArialMT" font-size="10.00">Post#2</text>
|
36
|
+
</g>
|
37
|
+
<!-- Category#1->Post#2 -->
|
38
|
+
<g id="edge2" class="edge">
|
39
|
+
<title>Category#1->Post#2</title>
|
40
|
+
<path fill="none" stroke="black" d="M75.24,-145.12C95.75,-134.36 120.36,-121.44 140.07,-111.1"/>
|
41
|
+
<polygon fill="black" stroke="black" points="73.71,-142.37 67.2,-149.34 76.64,-147.94 73.71,-142.37"/>
|
42
|
+
<polygon fill="black" stroke="black" points="141.68,-113.81 148.19,-106.83 138.76,-108.23 141.68,-113.81"/>
|
43
|
+
</g>
|
44
|
+
<!-- Category#2 -->
|
45
|
+
<g id="node2" class="node">
|
46
|
+
<title>Category#2</title>
|
47
|
+
<path fill="none" stroke="black" d="M153,-0.5C153,-0.5 198,-0.5 198,-0.5 204,-0.5 210,-6.5 210,-12.5 210,-12.5 210,-24.5 210,-24.5 210,-30.5 204,-36.5 198,-36.5 198,-36.5 153,-36.5 153,-36.5 147,-36.5 141,-30.5 141,-24.5 141,-24.5 141,-12.5 141,-12.5 141,-6.5 147,-0.5 153,-0.5"/>
|
48
|
+
<text text-anchor="start" x="150.5" y="-15.5" font-family="ArialMT" font-size="10.00">Category#2</text>
|
49
|
+
</g>
|
50
|
+
<!-- Author#1 -->
|
51
|
+
<g id="node3" class="node">
|
52
|
+
<title>Author#1</title>
|
53
|
+
<path fill="none" stroke="black" d="M110,-146.5C110,-146.5 159,-146.5 159,-146.5 165,-146.5 171,-152.5 171,-158.5 171,-158.5 171,-176.5 171,-176.5 171,-182.5 165,-188.5 159,-188.5 159,-188.5 110,-188.5 110,-188.5 104,-188.5 98,-182.5 98,-176.5 98,-176.5 98,-158.5 98,-158.5 98,-152.5 104,-146.5 110,-146.5"/>
|
54
|
+
<text text-anchor="start" x="107.5" y="-174.1" font-family="Monaco" font-size="8.00">let(:author)</text>
|
55
|
+
<text text-anchor="start" x="115" y="-157.5" font-family="ArialMT" font-size="10.00">Author#1</text>
|
56
|
+
</g>
|
57
|
+
<!-- Author#1->Post#1 -->
|
58
|
+
<g id="edge4" class="edge">
|
59
|
+
<title>Author#1->Post#1</title>
|
60
|
+
<path fill="none" stroke="black" d="M118.56,-138.42C113.08,-128.76 107.1,-118.22 102.25,-109.69"/>
|
61
|
+
<polygon fill="black" stroke="black" points="115.84,-140.01 123.02,-146.28 121.32,-136.9 115.84,-140.01"/>
|
62
|
+
</g>
|
63
|
+
<!-- Author#1->Post#2 -->
|
64
|
+
<g id="edge5" class="edge">
|
65
|
+
<title>Author#1->Post#2</title>
|
66
|
+
<path fill="none" stroke="black" d="M150.22,-138.13C155.53,-128.55 161.3,-118.14 165.98,-109.69"/>
|
67
|
+
<polygon fill="black" stroke="black" points="147.31,-136.88 145.7,-146.28 152.82,-139.93 147.31,-136.88"/>
|
68
|
+
</g>
|
69
|
+
<!-- Post#2->Category#2 -->
|
70
|
+
<g id="edge8" class="edge">
|
71
|
+
<title>Post#2->Category#2</title>
|
72
|
+
<path fill="none" stroke="black" d="M175.5,-64.13C175.5,-58.07 175.5,-51.64 175.5,-45.59"/>
|
73
|
+
<polygon fill="black" stroke="black" points="172.35,-64.31 175.5,-73.31 178.65,-64.31 172.35,-64.31"/>
|
74
|
+
<polygon fill="black" stroke="black" points="178.65,-45.53 175.5,-36.53 172.35,-45.53 178.65,-45.53"/>
|
75
|
+
</g>
|
76
|
+
<!-- Blog#1 -->
|
77
|
+
<g id="node6" class="node">
|
78
|
+
<title>Blog#1</title>
|
79
|
+
<path fill="none" stroke="black" d="M212.5,-146.5C212.5,-146.5 252.5,-146.5 252.5,-146.5 258.5,-146.5 264.5,-152.5 264.5,-158.5 264.5,-158.5 264.5,-176.5 264.5,-176.5 264.5,-182.5 258.5,-188.5 252.5,-188.5 252.5,-188.5 212.5,-188.5 212.5,-188.5 206.5,-188.5 200.5,-182.5 200.5,-176.5 200.5,-176.5 200.5,-158.5 200.5,-158.5 200.5,-152.5 206.5,-146.5 212.5,-146.5"/>
|
80
|
+
<text text-anchor="start" x="209.5" y="-174.1" font-family="Monaco" font-size="8.00">let(:blog)</text>
|
81
|
+
<text text-anchor="start" x="217" y="-157.5" font-family="ArialMT" font-size="10.00">Blog#1</text>
|
82
|
+
</g>
|
83
|
+
<!-- Blog#1->Post#1 -->
|
84
|
+
<g id="edge9" class="edge">
|
85
|
+
<title>Blog#1->Post#1</title>
|
86
|
+
<path fill="none" stroke="black" d="M192.4,-145.3C169.12,-133 140.41,-117.83 119.75,-106.91"/>
|
87
|
+
<polygon fill="black" stroke="black" points="190.95,-148.1 200.38,-149.52 193.9,-142.53 190.95,-148.1"/>
|
88
|
+
</g>
|
89
|
+
<!-- Blog#1->Post#2 -->
|
90
|
+
<g id="edge10" class="edge">
|
91
|
+
<title>Blog#1->Post#2</title>
|
92
|
+
<path fill="none" stroke="black" d="M211.32,-139C203.75,-129.18 195.45,-118.39 188.74,-109.69"/>
|
93
|
+
<polygon fill="black" stroke="black" points="208.94,-141.07 216.92,-146.28 213.93,-137.23 208.94,-141.07"/>
|
94
|
+
</g>
|
95
|
+
</g>
|
96
|
+
</svg>
|
data/docs/img/models.svg
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
3
|
+
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
4
|
+
<!-- Generated by graphviz version 3.0.0 (20220226.1711)
|
5
|
+
-->
|
6
|
+
<!-- Title: g Pages: 1 -->
|
7
|
+
<svg width="297pt" height="241pt"
|
8
|
+
viewBox="0.00 0.00 297.10 240.60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
9
|
+
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 211.8)">
|
10
|
+
<title>g</title>
|
11
|
+
<polygon fill="white" stroke="transparent" points="-28.8,28.8 -28.8,-211.8 268.3,-211.8 268.3,28.8 -28.8,28.8"/>
|
12
|
+
<!-- Category#1 -->
|
13
|
+
<g id="node1" class="node">
|
14
|
+
<title>Category#1</title>
|
15
|
+
<path fill="none" stroke="black" d="M12,-146.5C12,-146.5 57,-146.5 57,-146.5 63,-146.5 69,-152.5 69,-158.5 69,-158.5 69,-170.5 69,-170.5 69,-176.5 63,-182.5 57,-182.5 57,-182.5 12,-182.5 12,-182.5 6,-182.5 0,-176.5 0,-170.5 0,-170.5 0,-158.5 0,-158.5 0,-152.5 6,-146.5 12,-146.5"/>
|
16
|
+
<text text-anchor="start" x="9.5" y="-161.5" font-family="ArialMT" font-size="10.00">Category#1</text>
|
17
|
+
</g>
|
18
|
+
<!-- Post#1 -->
|
19
|
+
<g id="node4" class="node">
|
20
|
+
<title>Post#1</title>
|
21
|
+
<path fill="none" stroke="black" d="M70.5,-73.5C70.5,-73.5 100.5,-73.5 100.5,-73.5 106.5,-73.5 112.5,-79.5 112.5,-85.5 112.5,-85.5 112.5,-97.5 112.5,-97.5 112.5,-103.5 106.5,-109.5 100.5,-109.5 100.5,-109.5 70.5,-109.5 70.5,-109.5 64.5,-109.5 58.5,-103.5 58.5,-97.5 58.5,-97.5 58.5,-85.5 58.5,-85.5 58.5,-79.5 64.5,-73.5 70.5,-73.5"/>
|
22
|
+
<text text-anchor="start" x="70.5" y="-88.5" font-family="ArialMT" font-size="10.00">Post#1</text>
|
23
|
+
</g>
|
24
|
+
<!-- Category#1->Post#1 -->
|
25
|
+
<g id="edge1" class="edge">
|
26
|
+
<title>Category#1->Post#1</title>
|
27
|
+
<path fill="none" stroke="black" d="M52.19,-138.87C57.3,-131.76 62.87,-124 67.97,-116.91"/>
|
28
|
+
<polygon fill="black" stroke="black" points="49.54,-137.17 46.85,-146.31 54.65,-140.84 49.54,-137.17"/>
|
29
|
+
<polygon fill="black" stroke="black" points="70.58,-118.68 73.27,-109.53 65.46,-115 70.58,-118.68"/>
|
30
|
+
</g>
|
31
|
+
<!-- Post#2 -->
|
32
|
+
<g id="node5" class="node">
|
33
|
+
<title>Post#2</title>
|
34
|
+
<path fill="none" stroke="black" d="M153.5,-73.5C153.5,-73.5 183.5,-73.5 183.5,-73.5 189.5,-73.5 195.5,-79.5 195.5,-85.5 195.5,-85.5 195.5,-97.5 195.5,-97.5 195.5,-103.5 189.5,-109.5 183.5,-109.5 183.5,-109.5 153.5,-109.5 153.5,-109.5 147.5,-109.5 141.5,-103.5 141.5,-97.5 141.5,-97.5 141.5,-85.5 141.5,-85.5 141.5,-79.5 147.5,-73.5 153.5,-73.5"/>
|
35
|
+
<text text-anchor="start" x="153.5" y="-88.5" font-family="ArialMT" font-size="10.00">Post#2</text>
|
36
|
+
</g>
|
37
|
+
<!-- Category#1->Post#2 -->
|
38
|
+
<g id="edge2" class="edge">
|
39
|
+
<title>Category#1->Post#2</title>
|
40
|
+
<path fill="none" stroke="black" d="M74.6,-142.26C93.24,-132.38 115.22,-120.73 133.22,-111.19"/>
|
41
|
+
<polygon fill="black" stroke="black" points="73.07,-139.5 66.6,-146.49 76.02,-145.06 73.07,-139.5"/>
|
42
|
+
<polygon fill="black" stroke="black" points="134.71,-113.97 141.19,-106.97 131.76,-108.4 134.71,-113.97"/>
|
43
|
+
</g>
|
44
|
+
<!-- Category#2 -->
|
45
|
+
<g id="node2" class="node">
|
46
|
+
<title>Category#2</title>
|
47
|
+
<path fill="none" stroke="black" d="M146,-0.5C146,-0.5 191,-0.5 191,-0.5 197,-0.5 203,-6.5 203,-12.5 203,-12.5 203,-24.5 203,-24.5 203,-30.5 197,-36.5 191,-36.5 191,-36.5 146,-36.5 146,-36.5 140,-36.5 134,-30.5 134,-24.5 134,-24.5 134,-12.5 134,-12.5 134,-6.5 140,-0.5 146,-0.5"/>
|
48
|
+
<text text-anchor="start" x="143.5" y="-15.5" font-family="ArialMT" font-size="10.00">Category#2</text>
|
49
|
+
</g>
|
50
|
+
<!-- Author#1 -->
|
51
|
+
<g id="node3" class="node">
|
52
|
+
<title>Author#1</title>
|
53
|
+
<path fill="none" stroke="black" d="M110.5,-146.5C110.5,-146.5 144.5,-146.5 144.5,-146.5 150.5,-146.5 156.5,-152.5 156.5,-158.5 156.5,-158.5 156.5,-170.5 156.5,-170.5 156.5,-176.5 150.5,-182.5 144.5,-182.5 144.5,-182.5 110.5,-182.5 110.5,-182.5 104.5,-182.5 98.5,-176.5 98.5,-170.5 98.5,-170.5 98.5,-158.5 98.5,-158.5 98.5,-152.5 104.5,-146.5 110.5,-146.5"/>
|
54
|
+
<text text-anchor="start" x="107.5" y="-161.5" font-family="ArialMT" font-size="10.00">Author#1</text>
|
55
|
+
</g>
|
56
|
+
<!-- Author#1->Post#1 -->
|
57
|
+
<g id="edge4" class="edge">
|
58
|
+
<title>Author#1->Post#1</title>
|
59
|
+
<path fill="none" stroke="black" d="M112.59,-138.29C106.95,-128.76 100.67,-118.14 95.57,-109.53"/>
|
60
|
+
<polygon fill="black" stroke="black" points="110.04,-140.17 117.33,-146.31 115.46,-136.96 110.04,-140.17"/>
|
61
|
+
</g>
|
62
|
+
<!-- Author#1->Post#2 -->
|
63
|
+
<g id="edge5" class="edge">
|
64
|
+
<title>Author#1->Post#2</title>
|
65
|
+
<path fill="none" stroke="black" d="M142.06,-138.29C147.56,-128.76 153.69,-118.14 158.67,-109.53"/>
|
66
|
+
<polygon fill="black" stroke="black" points="139.2,-136.94 137.42,-146.31 144.65,-140.09 139.2,-136.94"/>
|
67
|
+
</g>
|
68
|
+
<!-- Post#2->Category#2 -->
|
69
|
+
<g id="edge8" class="edge">
|
70
|
+
<title>Post#2->Category#2</title>
|
71
|
+
<path fill="none" stroke="black" d="M168.5,-64.13C168.5,-58.07 168.5,-51.64 168.5,-45.59"/>
|
72
|
+
<polygon fill="black" stroke="black" points="165.35,-64.31 168.5,-73.31 171.65,-64.31 165.35,-64.31"/>
|
73
|
+
<polygon fill="black" stroke="black" points="171.65,-45.53 168.5,-36.53 165.35,-45.53 171.65,-45.53"/>
|
74
|
+
</g>
|
75
|
+
<!-- Blog#1 -->
|
76
|
+
<g id="node6" class="node">
|
77
|
+
<title>Blog#1</title>
|
78
|
+
<path fill="none" stroke="black" d="M197.5,-146.5C197.5,-146.5 227.5,-146.5 227.5,-146.5 233.5,-146.5 239.5,-152.5 239.5,-158.5 239.5,-158.5 239.5,-170.5 239.5,-170.5 239.5,-176.5 233.5,-182.5 227.5,-182.5 227.5,-182.5 197.5,-182.5 197.5,-182.5 191.5,-182.5 185.5,-176.5 185.5,-170.5 185.5,-170.5 185.5,-158.5 185.5,-158.5 185.5,-152.5 191.5,-146.5 197.5,-146.5"/>
|
79
|
+
<text text-anchor="start" x="197.5" y="-161.5" font-family="ArialMT" font-size="10.00">Blog#1</text>
|
80
|
+
</g>
|
81
|
+
<!-- Blog#1->Post#1 -->
|
82
|
+
<g id="edge9" class="edge">
|
83
|
+
<title>Blog#1->Post#1</title>
|
84
|
+
<path fill="none" stroke="black" d="M177.14,-143.73C156.75,-132.33 131.52,-118.23 112.7,-107.71"/>
|
85
|
+
<polygon fill="black" stroke="black" points="175.86,-146.63 185.26,-148.27 178.94,-141.13 175.86,-146.63"/>
|
86
|
+
</g>
|
87
|
+
<!-- Blog#1->Post#2 -->
|
88
|
+
<g id="edge10" class="edge">
|
89
|
+
<title>Blog#1->Post#2</title>
|
90
|
+
<path fill="none" stroke="black" d="M197.06,-138.58C191.1,-128.97 184.44,-118.23 179.05,-109.53"/>
|
91
|
+
<polygon fill="black" stroke="black" points="194.43,-140.32 201.85,-146.31 199.79,-137 194.43,-140.32"/>
|
92
|
+
</g>
|
93
|
+
</g>
|
94
|
+
</svg>
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "snapbot/reflector"
|
4
|
-
require "snapbot/diagram/dot_generator/relationship"
|
5
4
|
|
6
5
|
if defined?(::RSpec)
|
7
6
|
require "snapbot/rspec/lets"
|
@@ -13,15 +12,17 @@ module Snapbot
|
|
13
12
|
# Get a visual handle on what small constellations of objects we're creating
|
14
13
|
# in specs
|
15
14
|
class DotGenerator
|
16
|
-
def initialize(label: "g", attrs: false, ignore_lets: %i[])
|
15
|
+
def initialize(label: "g", attrs: false, ignore_lets: %i[], rspec: false)
|
17
16
|
@label = label
|
18
17
|
@options = { attrs: attrs }
|
19
18
|
@ignore_lets = ignore_lets
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
@lets_by_value = if rspec
|
21
|
+
example = binding.of_caller(1).eval("self")
|
22
|
+
collect_lets(example)
|
23
|
+
else
|
24
|
+
{}
|
25
|
+
end
|
25
26
|
end
|
26
27
|
|
27
28
|
def dot
|
@@ -38,7 +39,7 @@ module Snapbot
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def collect_lets(example)
|
41
|
-
|
42
|
+
RSpec::Lets.new(example).collect.each_with_object({}) do |sym, lets_by_value|
|
42
43
|
value = example.send(sym) unless @ignore_lets.include?(sym)
|
43
44
|
lets_by_value[value] = sym if value.is_a?(reflector.base_activerecord_class)
|
44
45
|
end
|
@@ -70,7 +71,7 @@ module Snapbot
|
|
70
71
|
label=<
|
71
72
|
<table border="0" cellborder="0">
|
72
73
|
<%- if @lets_by_value[instance] -%>
|
73
|
-
<tr><td
|
74
|
+
<tr><td><font face="Monaco,Courier,monospace" point-size="8">let(:<%= @lets_by_value[instance] %>)</font></td></tr>
|
74
75
|
<%- end -%>
|
75
76
|
<tr><td><%= instance_name(instance) %></td></tr>
|
76
77
|
</table>
|
@@ -79,9 +80,11 @@ module Snapbot
|
|
79
80
|
<table border="0" cellborder="0">
|
80
81
|
<%- attributes(instance).each_pair do |attr, value| -%>
|
81
82
|
<tr>
|
82
|
-
<td align="left"
|
83
|
+
<td align="left" port="<%= attr %>">
|
83
84
|
<%= attr %>
|
84
|
-
|
85
|
+
</td>
|
86
|
+
<td align="left">
|
87
|
+
<font color="grey50"><%= value.inspect %></font>
|
85
88
|
</td>
|
86
89
|
</tr>
|
87
90
|
<%- end -%>
|
data/lib/snapbot/diagram.rb
CHANGED
data/lib/snapbot/reflector.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_record"
|
4
|
+
require "snapbot/reflector/relationship"
|
4
5
|
|
5
6
|
module Snapbot
|
6
7
|
# Reflect models and instances in a way that's useful for generating a diagram
|
@@ -13,11 +14,21 @@ module Snapbot
|
|
13
14
|
@models ||= begin
|
14
15
|
Rails.application.eager_load! if defined?(Rails)
|
15
16
|
base_activerecord_class.descendants.reject do |c|
|
16
|
-
c
|
17
|
+
activerecord_ignore?(c) || (only_with_records && c.count.zero?)
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
22
|
+
ACTIVERECORD_IGNORE = [
|
23
|
+
/^Schema$/,
|
24
|
+
/^HABTM_/,
|
25
|
+
/^ActiveRecord::InternalMetadata$/,
|
26
|
+
/^ActiveRecord::SchemaMigration$/
|
27
|
+
].freeze
|
28
|
+
def activerecord_ignore?(klass)
|
29
|
+
ACTIVERECORD_IGNORE.any? { |r| klass.name =~ r } || klass.abstract_class
|
30
|
+
end
|
31
|
+
|
21
32
|
def instances
|
22
33
|
@instances ||= models.each_with_object([]) do |klass, array|
|
23
34
|
array << klass.all
|
@@ -43,7 +54,8 @@ module Snapbot
|
|
43
54
|
def reflect_associations(instance)
|
44
55
|
(
|
45
56
|
instance.class.reflect_on_all_associations(:has_many).reject { |a| a.name == :schemas } +
|
46
|
-
instance.class.reflect_on_all_associations(:has_one)
|
57
|
+
instance.class.reflect_on_all_associations(:has_one) +
|
58
|
+
instance.class.reflect_on_all_associations(:has_and_belongs_to_many)
|
47
59
|
).flatten
|
48
60
|
end
|
49
61
|
|
data/lib/snapbot/version.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Snapbot
|
2
|
+
module Diagram
|
3
|
+
# Get a visual handle on what small constellations of objects we're creating
|
4
|
+
# in specs
|
5
|
+
class DotGenerator
|
6
|
+
def initialize: (?label: ::String, ?attrs: bool, ?ignore_lets: Array[Symbol], ?rspec: bool) -> void
|
7
|
+
def dot: () -> ::String
|
8
|
+
|
9
|
+
@label: String
|
10
|
+
@attrs: bool
|
11
|
+
@ignore_lets: Array[Symbol]
|
12
|
+
|
13
|
+
@options: Hash[Object, Object]
|
14
|
+
@lets_by_value: Hash[Object, Symbol]
|
15
|
+
@reflector: Reflector
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def reflector: () -> Reflector
|
20
|
+
def collect_lets: (::RSpec::Core::ExampleGroup example) -> untyped
|
21
|
+
def instance_name: (ActiveRecord::Base `instance`) -> ::String
|
22
|
+
def template: () -> ::String
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Snapbot
|
2
|
+
module Diagram
|
3
|
+
# Render some DOT via Graphviz dot command line
|
4
|
+
class Renderer
|
5
|
+
INSTALL_GRAPHVIZ_URL: "https://graphviz.org/download/#executable-packages"
|
6
|
+
OUTPUT_FILENAME: "tmp/models.svg"
|
7
|
+
|
8
|
+
@dot: String
|
9
|
+
|
10
|
+
def initialize: (String dot) -> void
|
11
|
+
def save: () -> String
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def graphviz_executable: () -> ::String
|
16
|
+
def ensure_graphviz: () -> void
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Snapbot
|
2
|
+
# Print the small constellation of objects in your integration test and how they relate.
|
3
|
+
# Requires Graphviz. Optimised for Mac. YMMV.
|
4
|
+
module Diagram
|
5
|
+
def save_and_open_diagram: (**untyped args) -> (nil | untyped)
|
6
|
+
|
7
|
+
def open_command: () -> untyped
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Snapbot
|
2
|
+
class Reflector
|
3
|
+
# A source/destination-based relationship
|
4
|
+
class Relationship
|
5
|
+
attr_accessor source: ::String
|
6
|
+
|
7
|
+
attr_accessor destination: ::String
|
8
|
+
|
9
|
+
def initialize: (::String source, ::String destination) -> void
|
10
|
+
|
11
|
+
def ==: (Relationship other) -> bool
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
end
|
4
|
+
|
5
|
+
module Reflection
|
6
|
+
class AssociationReflection
|
7
|
+
def name: () -> Symbol
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Snapbot
|
13
|
+
# Reflect models and instances in a way that's useful for generating a diagram
|
14
|
+
class Reflector
|
15
|
+
ACTIVERECORD_IGNORE: ::Array[::Regexp]
|
16
|
+
|
17
|
+
@models: Array[Class]
|
18
|
+
@instances: Array[ActiveRecord::Base]
|
19
|
+
@relationships: Set[Relationship]
|
20
|
+
|
21
|
+
def base_activerecord_class: () -> Class
|
22
|
+
def models: (?only_with_records: bool) -> Array[Class]
|
23
|
+
def activerecord_ignore?: (Class klass) -> bool
|
24
|
+
def instances: () -> Array[ActiveRecord::Base]
|
25
|
+
|
26
|
+
# A Set of relationships to other identified entities
|
27
|
+
def relationships: () -> Set[Relationship]
|
28
|
+
def add_relationships: (ActiveRecord::Base `instance`, Set[Relationship] set) -> untyped
|
29
|
+
def reflect_associations: (ActiveRecord::Base `instance`) -> Array[ActiveRecord::Reflection::AssociationReflection]
|
30
|
+
def attributes: (ActiveRecord::Base `instance`) -> Hash[Symbol, Object]
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def instance_name: (ActiveRecord::Base `instance`) -> ::String
|
35
|
+
# Remains commented out, as is private, and https://github.com/ruby/rbs/issues/734#issuecomment-1192607498
|
36
|
+
# def escape_hash: (Hash[Symbol, Object] hash) -> untyped
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Core
|
3
|
+
class ExampleGroup
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module Snapbot
|
9
|
+
module RSpec
|
10
|
+
# Collect RSpec `let`s for a given example and all her parents
|
11
|
+
class Lets
|
12
|
+
@lets: Array[Symbol]
|
13
|
+
@lets_by_value: Hash[Object, Symbol]
|
14
|
+
@example: ::RSpec::Core::ExampleGroup
|
15
|
+
|
16
|
+
def initialize: (::RSpec::Core::ExampleGroup example) -> void
|
17
|
+
|
18
|
+
def collect: () -> Array[Symbol]
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def _collect: (::RSpec::Core::ExampleGroup klass, Array[Symbol] lets) -> Array[Symbol]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/snapbot.gemspec
CHANGED
@@ -35,7 +35,9 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_runtime_dependency "activerecord", version_string
|
36
36
|
spec.add_runtime_dependency "activesupport", version_string
|
37
37
|
|
38
|
+
spec.add_development_dependency "rbs"
|
38
39
|
spec.add_development_dependency "sqlite3"
|
40
|
+
spec.add_development_dependency "steep"
|
39
41
|
|
40
42
|
# For more information and examples about making a new gem, check out our
|
41
43
|
# guide at: https://bundler.io/guides/creating_gem.html
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: snapbot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Russell Garner
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-08-
|
11
|
+
date: 2022-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: binding_of_caller
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '6.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rbs
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: sqlite3
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,20 @@ dependencies:
|
|
80
94
|
- - ">="
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: steep
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description:
|
84
112
|
email:
|
85
113
|
- rgarner@zephyros-systems.co.uk
|
@@ -95,14 +123,24 @@ files:
|
|
95
123
|
- LICENSE.txt
|
96
124
|
- README.md
|
97
125
|
- Rakefile
|
126
|
+
- Steepfile
|
127
|
+
- docs/img/models-with-lets.svg
|
128
|
+
- docs/img/models.svg
|
98
129
|
- lib/snapbot.rb
|
99
130
|
- lib/snapbot/diagram.rb
|
100
131
|
- lib/snapbot/diagram/dot_generator.rb
|
101
|
-
- lib/snapbot/diagram/dot_generator/relationship.rb
|
102
132
|
- lib/snapbot/diagram/renderer.rb
|
103
133
|
- lib/snapbot/reflector.rb
|
134
|
+
- lib/snapbot/reflector/relationship.rb
|
104
135
|
- lib/snapbot/rspec/lets.rb
|
105
136
|
- lib/snapbot/version.rb
|
137
|
+
- lib/tasks/steep.rake
|
138
|
+
- sig/lib/snapbot/diagram.rbs
|
139
|
+
- sig/lib/snapbot/diagram/dot_generator.rbs
|
140
|
+
- sig/lib/snapbot/diagram/renderer.rbs
|
141
|
+
- sig/lib/snapbot/reflector.rbs
|
142
|
+
- sig/lib/snapbot/reflector/relationship.rbs
|
143
|
+
- sig/lib/snapbot/rspec/lets.rbs
|
106
144
|
- sig/snapbot.rbs
|
107
145
|
- snapbot.gemspec
|
108
146
|
homepage: https://github.com/rgarner/snapbot
|