specdiff 0.1.0 → 0.2.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: 7da7ac63f4638bd752f83ff41a27488757e43e57c028891d4fd944afd03f58ca
4
- data.tar.gz: d5a49e6aeb3525a454df920aa7d1b541a0812ffb9f8d0e1813a4bc05b57b006f
3
+ metadata.gz: 8d698240eae3e0f8f38c22b66df23efb1076b14879a7093c3d3e33aebddd37c9
4
+ data.tar.gz: 29e6198145c5a9ede875a26066a33e599561fe82277c6eb8bdb475c291304c9b
5
5
  SHA512:
6
- metadata.gz: 38ae83cd45a69892ca6d22915e0b253447eb12f42165a7673751abbb3993381702a02771ad3181a96d26204e6d171268a14423e85c2323a345df243be918f5a5
7
- data.tar.gz: f44ce432951f3e2198ed6c4ed4cfc975b4eb201ced235791cd917c2476ca7a60fcb4ce0aff023fd4423f97b7eee2819d395d514b970188a5bc232ce89ee44ea6
6
+ metadata.gz: cb5b52f47732689bc8322b840436d4c27fb048dbe20e9e0d8501b3120137500b7a9230a9818c31e049aa22715c251166d807b4e01111695406470c6b314c13c5
7
+ data.tar.gz: 103362c058c1e260dc9a9c3f1bd06b06d39ab25c1818e001c7160eb61ceb741d549b4f9c6fb974bb27614456ab2d34dfae6732bc6d842d42346bd62abe1fc331
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
1
8
  ## [Unreleased]
2
9
 
10
+ ## [0.2.0] - 2023-12-04
11
+
12
+ ### Changed
13
+
14
+ - Stop using thread locals ([#1](https://github.com/odinhb/specdiff/pull/1))
15
+
16
+ ## [0.1.1] - 2023-12-04
17
+
18
+ ### Fixed
19
+
20
+ - Fix #empty? not returning true when the diff is actually empty
21
+
3
22
  ## [0.1.0] - 2023-11-30
4
23
 
5
24
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- specdiff (0.1.0)
4
+ specdiff (0.2.0)
5
5
  diff-lcs (~> 1.5)
6
6
  hashdiff (~> 1.0)
7
7
 
data/README.md CHANGED
@@ -47,8 +47,10 @@ Specdiff.configure do |config|
47
47
  end
48
48
  ```
49
49
 
50
- The webmock patch should make webmock show diffs from the specdiff gem when
51
- stubs mismatch.
50
+ ### WebMock
51
+
52
+ The webmock patch should make webmock show request body diffs by using the
53
+ specdiff gem when stubs mismatch. It only applies to request bodies.
52
54
 
53
55
  ### Direct usage
54
56
 
@@ -61,18 +63,68 @@ diff.empty? # => true/false, if it is empty you might want to not print the diff
61
63
  diff.to_s # => a string for showing to a developer who may or may not be scratching their head
62
64
  ```
63
65
 
66
+ ### Registering plugins
67
+
68
+ ```rb
69
+ # Specdiff comes with json support, but it must be loaded like so:
70
+ Specdiff.load!(:json)
71
+
72
+ # Custom plugins can be loaded like this:
73
+ Specdiff.load!(MyCustomType)
74
+ ```
75
+
76
+ [Check out the source code](./lib/specdiff/plugins/json.rb) to learn the plugin interface.
77
+
64
78
  ## Development
65
79
 
80
+ Check out the [glossary](./glossary.txt) to make sure you (and I) are using the
81
+ same words for things ;)
82
+
66
83
  Install the software versions specified in `.tool-versions`.
67
84
 
68
85
  Run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests and linter and make sure they're green before starting to make your changes.
69
86
 
70
- Run `bundle exec rake -AD` gor a full list of all the available tasks you may use for development purposes.
87
+ Run `bundle exec rake -AD` for a full list of all the available tasks you may use for development purposes.
71
88
 
72
89
  You can also run `bin/console` for an interactive prompt that will allow you to experiment with the gem code loaded.
73
90
 
91
+ Remember to update the unreleased section of the [changelog](./CHANGELOG.md) before you submit your pull request.
92
+
93
+ ## How it works/"Architecture"
94
+
95
+ High level description of the heuristic specdiff implements
96
+
97
+ 1. receive 2 pieces of data: `a` and `b`
98
+ 2. determine types for `a` and `b`
99
+ 1. test against plugin types
100
+ 2. test against built in types
101
+ 3. fall back to the `:unknown` type
102
+ 3. determine which differ is appropriate for the types
103
+ 1. test against plugin differs
104
+ 2. test against built in differs
105
+ 3. fall back to the null differ (`NotFound`)
106
+ 7. run the selected differ with a and b
107
+ 8. package it into a `::Specdiff::Diff` which records the detected types
108
+
109
+ \<time passes>
110
+
111
+ 6. at some point later when `#to_s` is invoked, stringify the diff using the differ's `#stringify`
112
+
74
113
  ## Releasing
75
114
 
115
+ ### Release procedure
116
+
117
+ - [ ] unit tests are passing (`$ bundle exec rake test`)
118
+ - [ ] linter is happy (`$ bundle exec rake lint`)
119
+ - [ ] `$ cd examples/webmock && bundle install && bundle exec ruby json.rb` looks good
120
+ - [ ] `$ bundle exec ruby text.rb` looks good
121
+ - [ ] update the version number in `version.rb`
122
+ - [ ] make sure the `examples/` `Gemfile.lock` files are updated
123
+ - [ ] move unreleased changes to the next version in the [changelog](./CHANGELOG.md)
124
+ - [ ] commit in the form "vX.X.X" and push
125
+ - [ ] make sure the pipeline is green
126
+ - [ ] `$ bundle exec rake release`
127
+
76
128
  To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
77
129
 
78
130
  ## Contributing
@@ -82,3 +134,12 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/odinhb
82
134
  ## License
83
135
 
84
136
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
137
+
138
+ ## TODO
139
+
140
+ This documents potential improvements/issues I know about or have thought about.
141
+
142
+ - [ ] test the webmock monkey-patch. currently there is an empty webmock_spec.rb (should we do this using rspec?) and the examples/ directory contains a few webmock examples (which are a good idea to run before releasing) but it would be nice to have the pipeline fail if it doesn't work for whatever reason
143
+ - [ ] finalize plugin interface (are the methods named intuitively? should we split type detector definitions and differ definitions?)
144
+ - [ ] document how to define a plugin properly (instead of just linking to the source code)
145
+ - [ ] is the stringification of hashdiff's output really better than pretty print? or just more wordy? (the colors are definitely nice)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- specdiff (0.1.0)
4
+ specdiff (0.2.0)
5
5
  diff-lcs (~> 1.5)
6
6
  hashdiff (~> 1.0)
7
7
 
data/glossary.txt CHANGED
@@ -1,3 +1,8 @@
1
+ "raw diff"
2
+ the return value from a differ or plugin's #diff method
3
+ this can be anything, from an array of arrays (hashdiff) to a string with a
4
+ git diff inside
5
+
1
6
  diff
2
7
  the return value from the Specdiff::diff method.
3
8
  this is not the direct return value from a plugin/differ, that is the
@@ -8,17 +13,34 @@ differ
8
13
  human-comprehensible diff output for your terminal
9
14
 
10
15
  plugin
11
- external differ (responding to more methods), able to be provided by third
12
- parties or end users themselves
16
+ external differ (has to respond to more methods)
17
+ may be provided from outside the gem (for example by a user drowning in xml)
18
+
19
+ "built in differ"
20
+ differ living in the specdiff/differ directory
21
+
22
+ "built in plugin"
23
+ plugin shipped with the gem, but needs to be loaded using Specdiff.load!
13
24
 
14
25
  type
15
26
  a symbol like :text, :json or :hash which denotes the type of data in a way
16
27
  which is useful for picking a differ
17
28
 
29
+ a plugin returns a type from its #id method
30
+
18
31
  :text
19
32
  a string which likely contains plaintext data of some kind
20
33
 
34
+ "plugin type"
35
+ a type added by loading a plugin (not built into specdiff)
36
+
21
37
  side
22
- an object containing either side of a comparison that needs to be made
23
- when defining a plugin, the diff method receives two sides: a, b
24
- most importantly, a side contains a value and a type
38
+ an object containing a value and a type
39
+
40
+ used to represent the two sides to a comparison
41
+
42
+ when defining a plugin, you receive two sides: a and b, to various methods
43
+
44
+ compare
45
+ the procedure that implements the main function of specdiff including
46
+ accounting for any plugin types and differs
@@ -0,0 +1,95 @@
1
+ class Specdiff::Compare
2
+ Side = Struct.new(:value, :type, keyword_init: true)
3
+
4
+ def self.call(...)
5
+ new.call(...)
6
+ end
7
+
8
+ def call(raw_a, raw_b)
9
+ a = parse_side(raw_a)
10
+ b = parse_side(raw_b)
11
+
12
+ if a.type == :text && b.type == :binary
13
+ new_b = try_reencode(b.value, a.value.encoding)
14
+ if new_b
15
+ b = b.dup
16
+ b.type = :text
17
+ b.value = new_b
18
+ end
19
+ elsif a.type == :binary && b.type == :text
20
+ new_a = try_reencode(a.value, b.value.encoding)
21
+ if new_a
22
+ a = a.dup
23
+ a.type = :text
24
+ a.value = new_a
25
+ end
26
+ end
27
+
28
+ differ = pick_differ(a, b)
29
+ raw = differ.diff(a, b)
30
+
31
+ if raw.is_a?(::Specdiff::Diff) # detect recursive plugins, such as json
32
+ raw
33
+ else
34
+ ::Specdiff::Diff.new(raw: raw, differ: differ, a: a, b: b)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parse_side(raw_value)
41
+ type = detect_type(raw_value)
42
+
43
+ Side.new(value: raw_value, type: type)
44
+ end
45
+
46
+ def detect_type(thing)
47
+ if (type = detect_plugin_types(thing))
48
+ type
49
+ elsif thing.is_a?(Hash)
50
+ :hash
51
+ elsif thing.is_a?(Array)
52
+ :array
53
+ elsif thing.is_a?(String) && thing.encoding == Encoding::BINARY
54
+ :binary
55
+ elsif thing.is_a?(String)
56
+ :text
57
+ elsif thing.nil?
58
+ :nil
59
+ else
60
+ :unknown
61
+ end
62
+ end
63
+
64
+ def detect_plugin_types(thing)
65
+ Specdiff.plugins
66
+ .filter { |plugin| plugin.respond_to?(:detect_type) }
67
+ .detect { |plugin| plugin.detect_type(thing) }
68
+ &.id
69
+ end
70
+
71
+ def try_reencode(binary_string, target_encoding)
72
+ binary_string.encode(target_encoding)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+
77
+ def pick_differ(a, b)
78
+ if (differ = pick_plugin_differ(a, b))
79
+ differ
80
+ elsif a.type == :text && b.type == :text
81
+ Specdiff::Differ::Text
82
+ elsif a.type == :hash && b.type == :hash
83
+ Specdiff::Differ::Hashdiff
84
+ elsif a.type == :array && b.type == :array
85
+ Specdiff::Differ::Hashdiff
86
+ else
87
+ Specdiff::Differ::NotFound
88
+ end
89
+ end
90
+
91
+ def pick_plugin_differ(a, b)
92
+ Specdiff.plugins
93
+ .detect { |plugin| plugin.compatible?(a, b) }
94
+ end
95
+ end
@@ -5,23 +5,26 @@ module Specdiff
5
5
  end
6
6
  end
7
7
 
8
- # Read the configuration
9
- def self.config
10
- threadlocal[:config] ||= default_configuration
8
+ class << self
9
+ attr_reader :config
11
10
  end
12
11
 
12
+ DEFAULT = Config.new(colorize: true).freeze
13
+ @config = DEFAULT.dup
14
+
13
15
  # private, used for testing
14
16
  def self._set_config(new_config)
15
- threadlocal[:config] = new_config
17
+ @config = new_config
16
18
  end
17
19
 
18
20
  # Set the configuration
19
21
  def self.configure
20
- yield(config)
22
+ yield(@config)
23
+ @config
21
24
  end
22
25
 
23
26
  # Generates the default configuration
24
27
  def self.default_configuration
25
- Config.new(colorize: true)
28
+ DEFAULT
26
29
  end
27
30
  end
data/lib/specdiff/diff.rb CHANGED
@@ -10,7 +10,8 @@
10
10
  end
11
11
 
12
12
  def empty?
13
- differ == ::Specdiff::Differ::NotFound
13
+ differ == ::Specdiff::Differ::NotFound ||
14
+ (differ.respond_to?(:empty?) && differ.empty?(self))
14
15
  end
15
16
 
16
17
  def types
@@ -1,5 +1,4 @@
1
1
  require "hashdiff"
2
- require "pp"
3
2
 
4
3
  class Specdiff::Differ::Hashdiff
5
4
  extend ::Specdiff::Colorize
@@ -10,16 +9,18 @@ class Specdiff::Differ::Hashdiff
10
9
  # representation does not.
11
10
  # hmm it really seems like use_lcs: true gives much less human-readable
12
11
  # (human-comprehensible) output when arrays are involved.
13
- Hashdiff.diff(
12
+ ::Hashdiff.diff(
14
13
  a.value, b.value,
15
14
  array_path: true,
16
15
  use_lcs: false,
17
16
  )
18
17
  end
19
18
 
20
- def self.stringify(diff)
21
- diff.raw.pretty_inspect
19
+ def self.empty?(diff)
20
+ diff.raw.empty?
21
+ end
22
22
 
23
+ def self.stringify(diff)
23
24
  result = +""
24
25
 
25
26
  diff.raw.each do |change|
@@ -47,6 +47,8 @@ class Specdiff::Differ::Text
47
47
  diff << hunks.last.diff(:unified).to_s
48
48
  end
49
49
 
50
+ return diff if diff == ""
51
+
50
52
  diff << "\n"
51
53
 
52
54
  return colorize_by_line(diff) do |line|
@@ -67,6 +69,10 @@ class Specdiff::Differ::Text
67
69
  end
68
70
  end
69
71
 
72
+ def self.empty?(diff)
73
+ diff.raw == ""
74
+ end
75
+
70
76
  def self.stringify(diff)
71
77
  diff.raw
72
78
  end
@@ -1,97 +1,4 @@
1
- class Specdiff::Differ
2
- Side = Struct.new(:value, :type, keyword_init: true)
3
-
4
- def self.call(...)
5
- new.call(...)
6
- end
7
-
8
- def call(raw_a, raw_b)
9
- a = parse_side(raw_a)
10
- b = parse_side(raw_b)
11
-
12
- if a.type == :text && b.type == :binary
13
- new_b = try_reencode(b.value, a.value.encoding)
14
- if new_b
15
- b = b.dup
16
- b.type = :text
17
- b.value = new_b
18
- end
19
- elsif a.type == :binary && b.type == :text
20
- new_a = try_reencode(a.value, b.value.encoding)
21
- if new_a
22
- a = a.dup
23
- a.type = :text
24
- a.value = new_a
25
- end
26
- end
27
-
28
- differ = pick_differ(a, b)
29
- raw = differ.diff(a, b)
30
-
31
- if raw.is_a?(::Specdiff::Diff) # detect recursive plugins, such as json
32
- raw
33
- else
34
- ::Specdiff::Diff.new(raw: raw, differ: differ, a: a, b: b)
35
- end
36
- end
37
-
38
- private
39
-
40
- def parse_side(raw_value)
41
- type = detect_type(raw_value)
42
-
43
- Side.new(value: raw_value, type: type)
44
- end
45
-
46
- def detect_type(thing)
47
- if (type = detect_plugin_types(thing))
48
- type
49
- elsif thing.is_a?(Hash)
50
- :hash
51
- elsif thing.is_a?(Array)
52
- :array
53
- elsif thing.is_a?(String) && thing.encoding == Encoding::BINARY
54
- :binary
55
- elsif thing.is_a?(String)
56
- :text
57
- elsif thing.nil?
58
- :nil
59
- else
60
- :unknown
61
- end
62
- end
63
-
64
- def detect_plugin_types(thing)
65
- Specdiff.plugins
66
- .filter { |plugin| plugin.respond_to?(:detect_type) }
67
- .detect { |plugin| plugin.detect_type(thing) }
68
- &.id
69
- end
70
-
71
- def try_reencode(binary_string, target_encoding)
72
- binary_string.encode(target_encoding)
73
- rescue StandardError
74
- nil
75
- end
76
-
77
- def pick_differ(a, b)
78
- if (differ = pick_plugin_differ(a, b))
79
- differ
80
- elsif a.type == :text && b.type == :text
81
- Specdiff::Differ::Text
82
- elsif a.type == :hash && b.type == :hash
83
- Specdiff::Differ::Hashdiff
84
- elsif a.type == :array && b.type == :array
85
- Specdiff::Differ::Hashdiff
86
- else
87
- Specdiff::Differ::NotFound
88
- end
89
- end
90
-
91
- def pick_plugin_differ(a, b)
92
- Specdiff.plugins
93
- .detect { |plugin| plugin.compatible?(a, b) }
94
- end
1
+ module Specdiff::Differ
95
2
  end
96
3
 
97
4
  # require only the builtin differs, plugins are optionally loaded later
@@ -1,9 +1,8 @@
1
1
  module Specdiff
2
- THREADLOCAL_PLUGINS_KEY = :__specdiff_plugins
3
-
4
- def self.plugins
5
- threadlocal[THREADLOCAL_PLUGINS_KEY]
2
+ class << self
3
+ attr_reader :plugins
6
4
  end
5
+ @plugins = []
7
6
 
8
7
  BUILTIN_PLUGINS = %i[json]
9
8
  BUILTIN_TYPES = %i[hash array binary text nil]
@@ -59,14 +58,13 @@ module Specdiff
59
58
  MSG
60
59
  end
61
60
 
62
- ::Specdiff.plugins << plugin
61
+ @plugins << plugin
63
62
  end
64
63
 
65
64
  # private
66
65
  def self._clear_plugins!
67
- threadlocal[THREADLOCAL_PLUGINS_KEY] = []
66
+ @plugins = []
68
67
  end
69
- _clear_plugins!
70
68
 
71
69
  module Plugins
72
70
  end
@@ -1,3 +1,3 @@
1
1
  module Specdiff
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/specdiff.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  require_relative "specdiff/version"
2
- require_relative "specdiff/threadlocal"
3
2
  require_relative "specdiff/config"
4
3
  require_relative "specdiff/colorize"
4
+ require_relative "specdiff/compare"
5
5
 
6
6
  module Specdiff
7
7
  # Diff two things
8
8
  def self.diff(...)
9
- ::Specdiff::Differ.call(...)
9
+ ::Specdiff::Compare.call(...)
10
10
  end
11
11
  end
12
12
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: specdiff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Odin Heggvold Bekkelund
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-30 00:00:00.000000000 Z
11
+ date: 2023-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashdiff
@@ -61,6 +61,7 @@ files:
61
61
  - glossary.txt
62
62
  - lib/specdiff.rb
63
63
  - lib/specdiff/colorize.rb
64
+ - lib/specdiff/compare.rb
64
65
  - lib/specdiff/config.rb
65
66
  - lib/specdiff/diff.rb
66
67
  - lib/specdiff/differ.rb
@@ -70,7 +71,6 @@ files:
70
71
  - lib/specdiff/plugin.rb
71
72
  - lib/specdiff/plugins.rb
72
73
  - lib/specdiff/plugins/json.rb
73
- - lib/specdiff/threadlocal.rb
74
74
  - lib/specdiff/version.rb
75
75
  - lib/specdiff/webmock.rb
76
76
  - lib/specdiff/webmock/request_body_diff.rb
@@ -1,8 +0,0 @@
1
- module Specdiff
2
- THREADLOCAL_KEY = :__specdiff_threadlocals
3
-
4
- def self.threadlocal
5
- Thread.current.thread_variable_get(THREADLOCAL_KEY) ||
6
- Thread.current.thread_variable_set(THREADLOCAL_KEY, {})
7
- end
8
- end