specdiff 0.1.0 → 0.2.0

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