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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb58059025b35d29e5d762be3719f21461f225a2950c51efe54161009b1412ca
4
- data.tar.gz: a08b6fe2a72289b2946ac7461920863a7226a9d24b12a24d3b23dbe1f2ca00dd
3
+ metadata.gz: 4bd122996f917b0e18388a91aaeba183028d54a6b5e2fc58c05559f3a5103b6a
4
+ data.tar.gz: 15ce511a21b7d1dd96553a45959dce0e9ebfb9ad9ba2624d369af615742ec80b
5
5
  SHA512:
6
- metadata.gz: 31970a0125ab31512d051b4a8dac26e324fa556dd6ccfdffa1a83dc8bc9b6a962f5423150bcc634c45420cc59e12258b3d034b1210765b7c63cbba033b8a3277
7
- data.tar.gz: 530cebe2837a43ea3a9bddcdb63ed3bdda8666d3a2dd05cb79926e3d6f9c5b3c41da750e8d5dfc6c67fa3784c4f1c672b4620c0e2d135a1825bf87b1182b76fa
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
- 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 -%>
@@ -9,6 +9,7 @@ module Snapbot
9
9
  # Requires Graphviz. Optimised for Mac. YMMV.
10
10
  module Diagram
11
11
  def save_and_open_diagram(**args)
12
+ args.reverse_merge!(rspec: !!defined?(RSpec))
12
13
  dot = DotGenerator.new(**args).dot
13
14
  filename = Renderer.new(dot).save
14
15
 
@@ -11,7 +11,7 @@ module Snapbot
11
11
  self.destination = destination
12
12
  end
13
13
 
14
- def equals(other)
14
+ def ==(other)
15
15
  source == other.source && destination == other.destination
16
16
  end
17
17
  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
@@ -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.to_s == "Schema" || (only_with_records && c.count.zero?)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Snapbot
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.2"
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,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.1.3
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-08 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,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