snapbot 0.2.0 → 0.3.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 +4 -4
- data/.rubocop.yml +10 -0
- data/Gemfile +2 -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 +9 -7
- data/lib/snapbot/{diagram/dot_generator → reflector}/relationship.rb +0 -0
- data/lib/snapbot/reflector.rb +2 -1
- 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 +3 -0
- metadata +55 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b343dc1ae312d937e8999b491789a0335d79577e3a8d2ebcce764c42a4871a87
|
4
|
+
data.tar.gz: 89b3faeec45989c19e9a8f3497320ddac2a7728ee91d5c784557a1235bb58ea4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a66b1ddbad1589af9bbd087ceb201d50342fe8e252e6fa1b2107878fe2e1d2a9bc306ef72c6a15b720735e0eda76324148f2317808c7aeb44fc31300350e99d1
|
7
|
+
data.tar.gz: 18070e47081d20037e437a8d4c50eb50393edf23990d2102685792b51a43fde62eed90a9539cd37993d8941d080a5f44474d8e6223d500f4480109c973e81e1b
|
data/.rubocop.yml
CHANGED
@@ -1,6 +1,13 @@
|
|
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
|
4
11
|
|
5
12
|
Metrics/AbcSize:
|
6
13
|
Exclude:
|
@@ -15,6 +22,9 @@ Metrics/MethodLength:
|
|
15
22
|
Exclude:
|
16
23
|
- spec/support/fixture_database.rb
|
17
24
|
|
25
|
+
Style/DoubleNegation:
|
26
|
+
Enabled: false
|
27
|
+
|
18
28
|
Style/StderrPuts:
|
19
29
|
Exclude:
|
20
30
|
- lib/snapbot/diagram/renderer.rb
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
# Snapbot
|
2
2
|
|
3
|
+

|
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
|
+

|
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
|
+

|
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
@@ -2,27 +2,29 @@
|
|
2
2
|
|
3
3
|
require "snapbot/diagram/dot_generator"
|
4
4
|
require "snapbot/diagram/renderer"
|
5
|
-
require "open3"
|
6
5
|
|
7
6
|
module Snapbot
|
8
7
|
# Print the small constellation of objects in your integration test and how they relate.
|
9
8
|
# Requires Graphviz. Optimised for Mac. YMMV.
|
10
9
|
module Diagram
|
11
10
|
def save_and_open_diagram(**args)
|
11
|
+
args.reverse_merge!(rspec: !!defined?(RSpec))
|
12
12
|
dot = DotGenerator.new(**args).dot
|
13
13
|
filename = Renderer.new(dot).save
|
14
14
|
|
15
|
-
unless
|
16
|
-
warn "
|
15
|
+
unless launchy_present?
|
16
|
+
warn "Cannot open diagram – install `launchy`. File saved to #{filename}"
|
17
17
|
return
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
raise stderr unless status.exitstatus.zero?
|
20
|
+
Launchy.open(Renderer::OUTPUT_FILENAME)
|
22
21
|
end
|
23
22
|
|
24
|
-
def
|
25
|
-
|
23
|
+
def launchy_present?
|
24
|
+
require "launchy"
|
25
|
+
true
|
26
|
+
rescue LoadError
|
27
|
+
false
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
File without changes
|
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
|
@@ -25,7 +26,7 @@ module Snapbot
|
|
25
26
|
/^ActiveRecord::SchemaMigration$/
|
26
27
|
].freeze
|
27
28
|
def activerecord_ignore?(klass)
|
28
|
-
ACTIVERECORD_IGNORE.any? { |r| klass.name =~ r }
|
29
|
+
ACTIVERECORD_IGNORE.any? { |r| klass.name =~ r } || klass.abstract_class
|
29
30
|
end
|
30
31
|
|
31
32
|
def instances
|
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,10 @@ 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 "launchy"
|
39
|
+
spec.add_development_dependency "rbs"
|
38
40
|
spec.add_development_dependency "sqlite3"
|
41
|
+
spec.add_development_dependency "steep"
|
39
42
|
|
40
43
|
# For more information and examples about making a new gem, check out our
|
41
44
|
# 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.3.0
|
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,34 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '6.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: launchy
|
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'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rbs
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: sqlite3
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +108,20 @@ dependencies:
|
|
80
108
|
- - ">="
|
81
109
|
- !ruby/object:Gem::Version
|
82
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: steep
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
83
125
|
description:
|
84
126
|
email:
|
85
127
|
- rgarner@zephyros-systems.co.uk
|
@@ -95,14 +137,24 @@ files:
|
|
95
137
|
- LICENSE.txt
|
96
138
|
- README.md
|
97
139
|
- Rakefile
|
140
|
+
- Steepfile
|
141
|
+
- docs/img/models-with-lets.svg
|
142
|
+
- docs/img/models.svg
|
98
143
|
- lib/snapbot.rb
|
99
144
|
- lib/snapbot/diagram.rb
|
100
145
|
- lib/snapbot/diagram/dot_generator.rb
|
101
|
-
- lib/snapbot/diagram/dot_generator/relationship.rb
|
102
146
|
- lib/snapbot/diagram/renderer.rb
|
103
147
|
- lib/snapbot/reflector.rb
|
148
|
+
- lib/snapbot/reflector/relationship.rb
|
104
149
|
- lib/snapbot/rspec/lets.rb
|
105
150
|
- lib/snapbot/version.rb
|
151
|
+
- lib/tasks/steep.rake
|
152
|
+
- sig/lib/snapbot/diagram.rbs
|
153
|
+
- sig/lib/snapbot/diagram/dot_generator.rbs
|
154
|
+
- sig/lib/snapbot/diagram/renderer.rbs
|
155
|
+
- sig/lib/snapbot/reflector.rbs
|
156
|
+
- sig/lib/snapbot/reflector/relationship.rbs
|
157
|
+
- sig/lib/snapbot/rspec/lets.rbs
|
106
158
|
- sig/snapbot.rbs
|
107
159
|
- snapbot.gemspec
|
108
160
|
homepage: https://github.com/rgarner/snapbot
|