snapbot 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb7768e6e3ac46cacf84d569a0d8026062418160d39c5ae1e214875ac0f4b26d
4
- data.tar.gz: 50467bc231c453c7c81129181f90c21759f5acb2f121fbaf07bfd5313dcaea80
3
+ metadata.gz: b343dc1ae312d937e8999b491789a0335d79577e3a8d2ebcce764c42a4871a87
4
+ data.tar.gz: 89b3faeec45989c19e9a8f3497320ddac2a7728ee91d5c784557a1235bb58ea4
5
5
  SHA512:
6
- metadata.gz: 0331b6031f3fe4e3ee09aef66a1030bfec968f5df8c7308c2dd09933e0c63cfad55e54dab10e7bf0e9dc568216d34bbd3f8ac32c5bd6e524750ad676fbb49988
7
- data.tar.gz: 06757de1f42e455f6b8565661426bba4a94e609d567a5d2a2c6d8cdb8be8e5aed4b16e97334a009d34bda3b400707b2796a9a4b88b507a407b58e1ca8df2a798
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
@@ -5,6 +5,8 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in snapbot.gemspec
6
6
  gemspec
7
7
 
8
+ gem "launchy", "~> 2.5.0"
9
+
8
10
  gem "rake", "~> 13.0"
9
11
 
10
12
  gem "rspec", "~> 3.0"
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
- Either `gem install snapbot` or add the gem to your project's `:test` group in the gemfile:
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&#45;&gt;Post#1 -->
25
+ <g id="edge1" class="edge">
26
+ <title>Category#1&#45;&gt;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&#45;&gt;Post#2 -->
38
+ <g id="edge2" class="edge">
39
+ <title>Category#1&#45;&gt;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&#45;&gt;Post#1 -->
58
+ <g id="edge4" class="edge">
59
+ <title>Author#1&#45;&gt;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&#45;&gt;Post#2 -->
64
+ <g id="edge5" class="edge">
65
+ <title>Author#1&#45;&gt;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&#45;&gt;Category#2 -->
70
+ <g id="edge8" class="edge">
71
+ <title>Post#2&#45;&gt;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&#45;&gt;Post#1 -->
84
+ <g id="edge9" class="edge">
85
+ <title>Blog#1&#45;&gt;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&#45;&gt;Post#2 -->
90
+ <g id="edge10" class="edge">
91
+ <title>Blog#1&#45;&gt;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>
@@ -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&#45;&gt;Post#1 -->
25
+ <g id="edge1" class="edge">
26
+ <title>Category#1&#45;&gt;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&#45;&gt;Post#2 -->
38
+ <g id="edge2" class="edge">
39
+ <title>Category#1&#45;&gt;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&#45;&gt;Post#1 -->
57
+ <g id="edge4" class="edge">
58
+ <title>Author#1&#45;&gt;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&#45;&gt;Post#2 -->
63
+ <g id="edge5" class="edge">
64
+ <title>Author#1&#45;&gt;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&#45;&gt;Category#2 -->
69
+ <g id="edge8" class="edge">
70
+ <title>Post#2&#45;&gt;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&#45;&gt;Post#1 -->
82
+ <g id="edge9" class="edge">
83
+ <title>Blog#1&#45;&gt;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&#45;&gt;Post#2 -->
88
+ <g id="edge10" class="edge">
89
+ <title>Blog#1&#45;&gt;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
- return unless defined?(::RSpec)
22
-
23
- example = binding.of_caller(1).eval("self")
24
- collect_lets(example)
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
- @lets_by_value = RSpec::Lets.new(example).collect.each_with_object({}) do |sym, lets_by_value|
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 fontsize="8"><font face="Monaco" point-size="8">let(:<%= @lets_by_value[instance] %>)</font></td></tr>
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" width="200" port="<%= attr %>">
83
+ <td align="left" port="<%= attr %>">
83
84
  <%= attr %>
84
- <font face="Arial BoldMT" color="grey60"><%= value.inspect %></font>
85
+ </td>
86
+ <td align="left">
87
+ <font color="grey50"><%= value.inspect %></font>
85
88
  </td>
86
89
  </tr>
87
90
  <%- end -%>
@@ -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 open_command.present?
16
- warn "No `open` command available. File saved to #{filename}"
15
+ unless launchy_present?
16
+ warn "Cannot open diagram install `launchy`. File saved to #{filename}"
17
17
  return
18
18
  end
19
19
 
20
- _stdout, stderr, status = Open3.capture3("#{open_command} #{filename}")
21
- raise stderr unless status.exitstatus.zero?
20
+ Launchy.open(Renderer::OUTPUT_FILENAME)
22
21
  end
23
22
 
24
- def open_command
25
- `which open`.chomp
23
+ def launchy_present?
24
+ require "launchy"
25
+ true
26
+ rescue LoadError
27
+ false
26
28
  end
27
29
  end
28
30
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Snapbot
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :steep do
4
+ desc "Run `steep check`"
5
+ task :check do
6
+ system("steep check", exception: true)
7
+ end
8
+ end
@@ -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.2.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-09 00:00:00.000000000 Z
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