snapbot 0.1.3 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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