crimp 0.2.0 → 1.0.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: 933033b434c42dd1f1bddcae7dab3a28ca3cd59ea6f3c1c9099f7b7b94f16eb4
4
- data.tar.gz: a48ac5e269d70ac652422d4959372acd2937df77ae3a970887642b67ee5ad4af
3
+ metadata.gz: c137825335bbd1636911bede1f6acba9dc001070c68cce11aa26d7bb1e79f120
4
+ data.tar.gz: 610febce9965c5c7f2efe2aea310613f7e854b9de8de3ae1263acf525f12f78a
5
5
  SHA512:
6
- metadata.gz: b270bad70530524c0110fa9e6e80e29f95751802d7732cb5c683aa3e610d4674485ed4196384f8b6312629dc2cd64d2a60de0cce301a2dd8520f38be9735f966
7
- data.tar.gz: 269d0964c13056187ded2be18fc748bbbe9fc9e85b8b5e67d165aa27e9a07c7ff9d8681ccfddd6bd70bbc172514e6976e881d14b9df3da2783a7e04d6d2d0e99
6
+ metadata.gz: 87a85aedb707739ea28220183e96dbbfb51ed84443314a5cb2459c36f9ea0dd2d1d4e1ddd6a61e6ec43730414d73a021961e3861f1aca4c9e13fb7d8025ed6d3
7
+ data.tar.gz: 64edcb40a960ad815a0b4c2006d89e091f27b4a1102ad622be001dcae0b324942493d6737bd7b7205945253710c94e1e977dc6699e9ef354cbd44046c15ae969
@@ -1 +1 @@
1
- jruby-1.7.23
1
+ 2.5.1
@@ -1,9 +1,11 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - "jruby"
4
+ - "2.3"
5
+ - "2.5"
4
6
  notifications:
5
7
  email:
6
8
  recipients:
7
- - kenoir@gmail.com
9
+ - D&ENewsFrameworksTeam@bbc.co.uk
8
10
  on_failure: change
9
11
  on_success: never
@@ -1,22 +1,21 @@
1
- Copyright (c) 2014 Robert Kenny
1
+ The MIT License
2
2
 
3
- MIT License
3
+ Copyright (c) 2014-2018 BBC News https://www.bbc.co.uk/news
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
12
11
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
15
14
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -3,45 +3,187 @@
3
3
  [![Build Status](https://travis-ci.org/BBC-News/crimp.png?branch=master)](https://travis-ci.org/BBC-News/crimp)
4
4
  [![Gem Version](https://badge.fury.io/rb/crimp.png)](http://badge.fury.io/rb/crimp)
5
5
 
6
- Creating an md5 hash of a number, string, array, or hash in Ruby
7
-
8
- ![mighty-boosh-four-way-crimp-o](https://f.cloud.github.com/assets/180050/2148112/b44fd6fa-93de-11e3-9f9a-ad941f069b5c.gif)
9
-
10
- Shamelessly copied from [this Stack Overflow
11
- answer](http://stackoverflow.com/a/6462589/3243663).
6
+ Creates an MD5 hash from simple data structures made of numbers, strings, booleans, nil, arrays or hashes.
12
7
 
13
8
  ## Installation
14
9
 
15
10
  Add this line to your application's Gemfile:
16
11
 
17
- gem 'crimp'
12
+ ```ruby
13
+ gem 'crimp'
14
+
15
+ ```
18
16
 
19
17
  And then execute:
20
18
 
21
- $ bundle
19
+ ```shell
20
+ $ bundle
21
+
22
+ ```
22
23
 
23
24
  Or install it yourself as:
24
25
 
25
- $ gem install crimp
26
+ ```shell
27
+ $ gem install crimp
28
+ ```
26
29
 
27
30
  ## Usage
28
31
 
29
- ```rb
32
+ ```ruby
30
33
  require 'crimp'
31
34
 
32
- Crimp.stringify({:a => {:b => 'b', :c => 'c'}, :d => 'd'})
35
+ Crimp.signature({ a: { b: 1 } })
36
+ => "ac13c15d07e5fa3992fc6b15113db900"
37
+ ```
38
+
39
+ ## Multiplatform design
40
+
41
+ At the BBC we use Crimp to build keys for database and cache entries.
42
+
43
+ If you want to build a similar library with your language of choice you should be able to follow the simple specifications defined in `spec/crimp_spec.rb`. Using these simple rules you will produce a string ready to be MD5 signed.
44
+
45
+ Once you get your string, is very important to be sure that you can produce the same key in any language. MD5 is your friend:
46
+
47
+ ### Ruby
48
+
49
+ ```ruby
50
+ irb(main):001:0> require 'digest'
51
+ => true
52
+ irb(main):002:0> Digest::MD5.hexdigest('abc')
53
+ => "900150983cd24fb0d6963f7d28e17f72"
54
+ ```
55
+
56
+ ### Lua
57
+
58
+ ```lua
59
+ Lua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio
60
+ > md5 = require 'md5'
61
+ > md5.sumhexa('abc')
62
+ 900150983cd24fb0d6963f7d28e17f72
63
+ ```
33
64
 
34
- # => [\"aSymbol=>[\\\"bSymbol=>b\\\", \\\"cSymbol=>c\\\"]Array\",\"dSymbol=>d\"]Array"
65
+ ### Elixir
35
66
 
36
- Crimp.signature({:a => {:b => 'b', :c => 'c'}, :d => 'd'})
67
+ ``` elixir
68
+ Erlang/OTP 21 [erts-10.0.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
69
+ Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
70
+ iex(1)> :crypto.hash(:md5 , "abc") |> Base.encode16() |> String.downcase
71
+ "900150983cd24fb0d6963f7d28e17f72"
72
+ ```
37
73
 
38
- # => "68d07febc4f47f56fa6ef5de063a77b1"
74
+ ### Node.js
39
75
 
76
+ ``` javascript
77
+ > var crypto = require('crypto');
78
+ undefined
79
+ > crypto.createHash('md5').update('abc').digest('hex');
80
+ '900150983cd24fb0d6963f7d28e17f72'
40
81
  ```
41
82
 
83
+ ## Fine prints
84
+
85
+ ### Symbols
86
+
87
+ To make Crimp signatures reproducible in any platform we decided to ignore Ruby symbols and treat them as strings, so:
88
+
89
+ ``` ruby
90
+ Crimp.signature(:a) == Crimp.Signature('a')
91
+ ```
92
+
93
+ ### Sets
94
+
95
+ Also Sets get transformed to Arrays:
96
+
97
+ ``` ruby
98
+ Crimp.signature(Set.new(['a', 'b'])) == Crimp.signature(['a', 'b'])
99
+ ```
100
+
101
+ ### Sorting of collections
102
+
103
+ Crimp signatures are generated against sorted collections.
104
+
105
+ ```ruby
106
+ Crimp.signature([1, 2]) == Crimp.signature([2, 1])
107
+ Crimp.signature({'b' => 2, 'a' => 1}) == Crimp.signature({'a' => 1, 'b' => 2})
108
+ ```
109
+
110
+ Crimp also sorts nested collections.
111
+
112
+ ```ruby
113
+ Crimp.signature([1, [3, 2], 4]) == Crimp.signature([4, [2, 3], 1])
114
+ Crimp.signature({'b' => {'d' => 2,'c' => 1}, 'a' => [3, 1, 2]}) == Crimp.signature({'a' => [1, 2, 3], 'b' => { 'c' => 1, 'd' => 2 }})
115
+ ```
116
+
117
+ ### Custom objects
118
+
119
+ Crimp will complain if you try to get a signature from an instance of some custom object:
120
+
121
+ ``` ruby
122
+ Crimp.signature(Object.new)
123
+ => TypeError: Expected a (String|Number|Boolean|Nil|Hash|Array), Got Object
124
+ ```
125
+ It is your responsibility to pass a compatible representation of your object to Crimp.
126
+
127
+ ## Implementation details
128
+
129
+ Under the hood Crimp annotates the passed data structure to a nested array of primitives (strings, numbers, booleans, nils, etc.) and a single byte to indicate the type of the primitive:
130
+
131
+ | Type | Byte |
132
+ | :-: | :-: |
133
+ | String | `S` |
134
+ | Number | `N` |
135
+ | Boolean | `B` |
136
+ | nil | `_` |
137
+ | Array | `A` |
138
+ | Hash | `H` |
139
+
140
+ You can verify it using the `#annotate` method:
141
+
142
+ ``` ruby
143
+ Crimp.annotate({ a: 1 })
144
+ => [[[[[1, "N"], ["a", "S"]], "A"]], "H"]
145
+ ```
146
+ Notice how Crimp marks the collection as Hash (`H`) and then transforms the tuple of key/values to an Array (`A`).
147
+
148
+ Here's an example with nested hashes:
149
+
150
+ ```ruby
151
+ Crimp.annotate({ a: { b: 'c' } })
152
+ => [[[[["a", "S"], [[[[["b", "S"], ["c", "S"]], "A"]], "H"]], "A"]], "H"]
153
+ ```
154
+
155
+ Before signing Crimp transforms the collection of nested array to a string.
156
+
157
+ ```ruby
158
+ Crimp.notation({ a: { b: 'c' } })
159
+ => "aSbScSAHAH"
160
+ ```
161
+
162
+ Please note the Arrays and Hash keys are sorted before signing.
163
+
164
+ ``` ruby
165
+ Crimp.notation([3, 1, 2])
166
+ => "1N2N3NA"
167
+ ```
168
+
169
+ key/value tuples get sorted as well.
170
+
171
+ ``` ruby
172
+ Crimp.notation({ a: 1 })
173
+ => "1NaSAH"
174
+ ```
175
+
176
+ ## Changelog
177
+
178
+ | Version | Changes |
179
+ |---------|----------------------------------------------------------------------------|
180
+ |`v0.x` | Original version of Crimp. |
181
+ |`v0.2.0` | Crimp compatibility with Ruby >= 2.4, use this for legacy projects. |
182
+ |`v1.0.0` | Includes **breaking changes** and returns different signatures from `v0.2` |
183
+
42
184
  ## Contributing
43
185
 
44
- 1. Fork it ( http://github.com/<my-github-username>/crimp/fork )
186
+ 1. Fork it ( http://github.com/BBC-News/crimp/fork )
45
187
  2. Create your feature branch (`git checkout -b my-new-feature`)
46
188
  3. Commit your changes (`git commit -am 'Add some feature'`)
47
189
  4. Push to the branch (`git push origin my-new-feature`)
data/Rakefile CHANGED
@@ -1,6 +1,10 @@
1
1
  $LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
2
2
 
3
+ require 'rspec/core/rake_task'
3
4
  require 'bundler/gem_tasks'
4
- require 'rake/rspec'
5
5
 
6
- task default: :spec
6
+ RSpec::Core::RakeTask.new :specs do |task|
7
+ task.pattern = Dir['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task default: [:specs]
@@ -1,35 +1,24 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'crimp/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "crimp"
8
- spec.version = Crimp::VERSION
9
- spec.authors = ["BBC News"]
10
- spec.email = ["D&ENewsFrameworksTeam@bbc.co.uk"]
11
- spec.summary = %q{Creating an md5 hash of a number, string, array, or hash in Ruby}
12
- spec.description = <<-EOS.gsub /^\s+/, ""
13
- Shamelessly lifted from http://stackoverflow.com/questions/6461812/creating-an-md5-hash-of-a-number-string-array-or-hash-in-ruby
14
-
15
- All credit should go to http://stackoverflow.com/users/394282/luke
16
- EOS
17
- spec.homepage = ""
18
- spec.license = "MIT"
6
+ spec.name = 'crimp'
7
+ spec.version = '1.0.0'
8
+ spec.authors = ['BBC News']
9
+ spec.email = ['D&ENewsFrameworksTeam@bbc.co.uk']
10
+ spec.summary = 'Creates an MD5 hash from simple data structures.'
11
+ spec.description = 'Platform agnostic MD5 hash from simple data structures made of numbers, strings, booleans, nil, arrays or hashes.'
12
+ spec.homepage = 'https://www.bbc.co.uk/news'
13
+ spec.license = 'MIT'
19
14
 
20
15
  spec.files = `git ls-files -z`.split("\x0")
21
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
- spec.require_paths = ["lib"]
18
+ spec.require_paths = ['lib']
24
19
 
25
- spec.add_development_dependency "bundler", "~> 1.5"
26
- spec.add_development_dependency "rake"
27
- spec.add_development_dependency "rspec"
28
- spec.add_development_dependency "rake-rspec"
29
- spec.add_development_dependency "rspec-nc"
30
- spec.add_development_dependency "guard"
31
- spec.add_development_dependency "guard-rspec"
32
- spec.add_development_dependency "pry"
33
- spec.add_development_dependency "pry-remote"
34
- spec.add_development_dependency "pry-nav"
20
+ spec.add_development_dependency 'bundler'
21
+ spec.add_development_dependency 'rake'
22
+ spec.add_development_dependency 'rspec'
23
+ spec.add_dependency 'deepsort'
35
24
  end
@@ -1,74 +1,52 @@
1
- require 'crimp/version'
2
- require 'digest'
1
+ # frozen_string_literal: true
3
2
 
4
- class Numeric
5
- # see http://patshaughnessy.net/2014/1/9/how-big-is-a-bignum
6
- def bignum?
7
- self >= 4611686018427387904
8
- end
9
- end
3
+ require 'digest/md5'
4
+ require 'set'
5
+ require 'deepsort'
10
6
 
11
- module Crimp
12
- def self.signature(obj)
13
- Digest::MD5.hexdigest stringify(obj)
14
- end
15
-
16
- def self.stringify(obj)
17
- convert(obj).tap { |o| return o.class == String ? o : to_string(o) }
18
- end
19
-
20
- private
21
-
22
- def self.convert(obj)
23
- case obj
24
- when Array
25
- parse_array obj
26
- when Hash
27
- parse_hash obj
28
- when String
29
- obj
30
- else
31
- to_string obj
7
+ class Crimp
8
+ class << self
9
+ def signature(obj)
10
+ Digest::MD5.hexdigest(notation(obj))
32
11
  end
33
- end
34
12
 
35
- def self.hash_to_array(hash)
36
- [].tap do |a|
37
- hash.each { |k, v| a << pair_to_string(k, v) }
13
+ def notation(obj)
14
+ annotate(obj).flatten.join
38
15
  end
39
- end
40
16
 
41
- def self.pair_to_string(k, v)
42
- "#{stringify k}=>#{stringify v}"
43
- end
44
-
45
- def self.parse_array(array)
46
- array.map { |e| stringify(e) }.sort
47
- end
17
+ def annotate(obj)
18
+ obj = coerce(obj)
19
+
20
+ case obj
21
+ when String
22
+ [obj, 'S']
23
+ when Numeric
24
+ [obj, 'N']
25
+ when TrueClass, FalseClass
26
+ [obj, 'B']
27
+ when NilClass
28
+ [nil, '_']
29
+ when Array
30
+ [sort(obj), 'A']
31
+ when Hash
32
+ [sort(obj), 'H']
33
+ else
34
+ raise TypeError, "Expected a (String|Number|Boolean|Nil|Hash|Array), Got #{obj.class}."
35
+ end
36
+ end
48
37
 
49
- def self.parse_hash(hash)
50
- stringify hash_to_array(hash)
51
- end
38
+ private
52
39
 
53
- def self.to_string(obj)
54
- "#{obj}#{legacy_class(obj)}"
55
- end
40
+ def sort(coll)
41
+ coll.deep_sort_by { |obj| obj.to_s }.map { |obj| annotate(obj) }
42
+ end
56
43
 
57
- # This is for legacy/compatibilty reason:
58
- #
59
- # Ruby 2.1
60
- # 2.class => Fixnum
61
- # Ruby >= 2.4
62
- # 2.class => Integer
63
- #
64
- # Say you have a huge number of stored keys and you migrate your app from 2.1 to >= 2.4
65
- # this would cause a change of the signature for a subset of the keys which would be hard
66
- # to debug especially for nested data structures.
67
- #
68
- def self.legacy_class(obj)
69
- return obj.class unless obj.is_a?(Numeric)
70
- return 'Float' if obj.is_a?(Float)
71
- return 'Bignum' if obj.bignum?
72
- 'Fixnum'
44
+ def coerce(obj)
45
+ case obj
46
+ when Symbol then obj.to_s
47
+ when Set then obj.to_a
48
+ else obj
49
+ end
50
+ end
73
51
  end
74
52
  end
@@ -0,0 +1,66 @@
1
+ ---
2
+ -
3
+ desc: verify String handling
4
+ json: abc
5
+ string: abcS
6
+ signature: c4449120506d97975c67be69719a78e2
7
+ -
8
+ desc: verify integers handling
9
+ json: 1
10
+ string: 1N
11
+ signature: 594170053719896a11eb08ee513813d5
12
+ -
13
+ desc: verify floats handling
14
+ json: 1.2
15
+ string: 1.2N
16
+ signature: f1ab6592886cd4b1b66ed55e73d9ab81
17
+ -
18
+ desc: verify Array handling
19
+ json: [1, "a", 3]
20
+ string: 1N3NaSA
21
+ signature: cd1c43797d488d0f6c0d71537c64d30b
22
+ -
23
+ desc: verify Array sorting
24
+ json: [3, null, 1, "1"]
25
+ string: _1N1S3NA
26
+ signature: 518e7bb17674f6acbb296845862a152d
27
+ -
28
+ desc: verify Array sorting with capital letters
29
+ json: ["a", "A", "b", "B"]
30
+ string: ASBSaSbSA
31
+ signature: f6692ab4bc94b35e61ec15c2d1891734
32
+ -
33
+ desc: verify nested Arrays
34
+ json: ["a", 1, ["b", "2"]]
35
+ string: 1N2SbSAaSA
36
+ signature: 3aaa58da4841eaeb41d3726d2c6fd875
37
+ -
38
+ desc: verify nested Arrays
39
+ json: [["b", "2"], "a", 1]
40
+ string: 1N2SbSAaSA
41
+ signature: 3aaa58da4841eaeb41d3726d2c6fd875
42
+ -
43
+ desc: verify hash like data structures
44
+ json: {"a": 1}
45
+ string: 1NaSAH
46
+ signature: 8cb44d69badda0f34b0bab6bb3e7fdbf
47
+ -
48
+ desc: verify nested hash
49
+ json: {"a": {"c": null, "2": 2 }}
50
+ string: aS2S2NA_cSAHAH
51
+ signature: bff3538075e4007c7679a7ba0d0a5f30
52
+ -
53
+ desc: verify null values
54
+ json: null
55
+ string: _
56
+ signature: b14a7b8059d9c055954c92674ce60032
57
+ -
58
+ desc: verify true boolean values
59
+ json: true
60
+ string: trueB
61
+ signature: 6413cfeb7a89f7e0a8872f82b919c0d9
62
+ -
63
+ desc: verify false boolean values
64
+ json: false
65
+ string: falseB
66
+ signature: fa39253035cfe44c8638b8f5d7a3402e
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+ require 'json'
4
+
5
+ # other libraries could fetch the test data from Github.
6
+ file = File.join(__dir__, 'acceptance_data.yml')
7
+ tests = YAML::load_file(file)
8
+
9
+ describe 'Multiplatform compatibility acceptance tests' do
10
+ tests.each do |test|
11
+ input = JSON.parse(test['json'].to_json, quirks_mode: true)
12
+ signature = test['signature']
13
+ string = test['string']
14
+
15
+ specify "#{test['desc']} string" do
16
+ expect(Crimp.notation(input)).to eq(string)
17
+ end
18
+
19
+ specify "#{test['desc']} signature" do
20
+ expect(Crimp.signature(input)).to eq(signature)
21
+ end
22
+ end
23
+ end
@@ -1,99 +1,299 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
- describe Crimp do
4
- let(:hash) { { a: { b: 'b', c: 'c' }, d: 'd' } }
5
- let(:hash_with_numbers) { { a: { b: 1, c: 3.14 }, d: 'd' } }
6
- let(:hash_unordered) { { d: 'd', a: { c: 'c', b: 'b' } } }
7
- let(:array) { [1, 2, 3, [4, [5, 6]]] }
8
- let(:array_unordered) { [3, 2, 1, [[5, 6], 4]] }
9
-
10
- describe '.signature' do
11
- context 'given a Hash' do
12
- it 'returns MD5 hash of stringified Hash' do
13
- expect(subject.signature(hash)).to eq('68d07febc4f47f56fa6ef5de063a77b1')
14
- end
15
-
16
- it 'does not modify original hash' do
17
- original_hash = { d: 'd', a: { c: 'c', b: 'b' } }
18
- expected_hash = { d: 'd', a: { c: 'c', b: 'b' } }
19
-
20
- subject.signature(original_hash)
21
-
22
- expect(original_hash).to eq(expected_hash)
23
- end
24
- end
25
-
26
- context 'Given an hash with numbers' do
27
- it 'returns MD5 hash of stringified hash' do
28
- expect(subject.signature(hash_with_numbers)).to eq 'b1fec09904b6ff36c92e3bd48234def7'
29
- end
30
- end
31
-
32
- context 'given an Array' do
33
- it 'returns MD5 hash of stringified Array' do
34
- expect(subject.signature(array)).to eq('4dc4e1161c9315db0bc43c0201b3ec05')
35
- end
36
-
37
- it 'does not modify original array' do
38
- original_array = [5, 4, 2, 6, [5, 7, 2]]
39
- expected_array = [5, 4, 2, 6, [5, 7, 2]]
40
-
41
- subject.signature(original_array)
42
-
43
- expect(original_array).to eq(expected_array)
44
- end
45
- end
46
-
47
- context 'Given an integer' do
48
- it 'returns MD5 hash of an Integer' do
49
- expect(subject.signature(123)).to eq '519d3381631851be66711f6d7dfbb4f8'
50
- end
51
- end
52
-
53
- context 'Given an Bignum' do
54
- it 'returns MD5 hash of a Bignum' do
55
- expect(subject.signature(9999999999999999999)).to eq 'f00e75abca720e18fd4213e2a6de96c6'
56
- end
57
- end
58
-
59
- context 'Given an float' do
60
- it 'returns MD5 hash of a Float' do
61
- expect(subject.signature(3.14)).to eq 'b07d506e3701fddd083ae9095df43218'
62
- end
63
- end
64
- end
65
-
66
- describe '.stringify' do
67
- context 'given a Hash' do
68
- it 'returns equal strings for differently ordered hashes' do
69
- expect(subject.stringify(hash)).to eq(subject.stringify(hash_unordered))
70
- end
71
-
72
- it 'does not modify original hash' do
73
- original_hash = { d: 'd', a: { c: 'c', b: 'b' } }
74
- expected_hash = { d: 'd', a: { c: 'c', b: 'b' } }
75
-
76
- subject.signature(original_hash)
77
-
78
- expect(original_hash).to eq(expected_hash)
79
- end
80
- end
81
-
82
- context 'given an Array' do
83
- specify { expect(subject.stringify(array)).to be_a String }
84
-
85
- it 'returns equal strings for differently ordered arrays' do
86
- expect(subject.stringify(array)).to eq(subject.stringify(array_unordered))
87
- end
88
-
89
- it 'does not modify original array' do
90
- original_array = [5, 4, 2, 6, [5, 7, 2]]
91
- expected_array = [5, 4, 2, 6, [5, 7, 2]]
92
-
93
- subject.signature(original_array)
94
-
95
- expect(original_array).to eq(expected_array)
96
- end
97
- end
5
+ describe '.signature' do
6
+ it 'will return an md5 hash' do
7
+ expect(Crimp.signature('a')).to eq 'd132c0567a5964930f9ee5f14e779e32'
8
+ end
9
+ end
10
+
11
+ describe '.notation' do
12
+ it 'returns a string representation of the passed data' do
13
+ expect(Crimp.notation([123, 'abc'])).to eq('123NabcSA')
14
+ end
15
+ end
16
+
17
+ describe '.annotate' do
18
+ it 'returns an array of tuples representing the value and the type' do
19
+ expect(Crimp.annotate([123, 'abc'])).to eq([[[123, 'N'], ['abc', 'S']], 'A'])
20
+ end
21
+
22
+ it "returns a tuple [val, 'N'] for numeric primitives" do
23
+ expect(Crimp.annotate(123)).to eq([123, 'N'])
24
+ end
25
+
26
+ it "returns a tuple [val, 'S'] for string primitives" do
27
+ expect(Crimp.annotate('abc')).to eq(['abc', 'S'])
28
+ end
29
+
30
+ it "returns a tuple [[], 'A'] for empty arrays" do
31
+ expect(Crimp.annotate([])).to eq([[], 'A'])
32
+ end
33
+ end
34
+
35
+ describe 'Strings' do
36
+ it 'handles strings' do
37
+ expect(Crimp.annotate('a')).to eq(['a', 'S'])
38
+ end
39
+
40
+ it 'handles capitalised strings with no modifications' do
41
+ expect(Crimp.annotate('A')).to eq(['A', 'S'])
42
+ end
43
+
44
+ it 'handles utf-8 strings' do
45
+ expect(Crimp.annotate('å')).to eq(['å', 'S'])
46
+ end
47
+
48
+ it 'treats symbols like strings' do
49
+ expect(Crimp.annotate(:a)).to eq(['a', 'S'])
50
+ end
51
+
52
+ it 'treats empty strings like strings' do
53
+ expect(Crimp.annotate('')).to eq(['', 'S'])
54
+ end
55
+ end
56
+
57
+ describe 'Numbers' do
58
+ it 'handles integers' do
59
+ expect(Crimp.annotate(1)).to eq([1, 'N'])
60
+ end
61
+
62
+ it 'handles floats' do
63
+ expect(Crimp.annotate(3.14)).to eq([3.14, 'N'])
64
+ end
65
+
66
+ it 'handles bignums' do
67
+ bignum = 10_000_000_000_000_000_000
68
+
69
+ expect(Crimp.annotate(bignum)).to eq([bignum, 'N'])
70
+ end
71
+ end
72
+
73
+ describe 'Nils' do
74
+ it 'handles nils' do
75
+ expect(Crimp.annotate(nil)).to eq([nil, '_'])
76
+ end
77
+ end
78
+
79
+ describe 'Booleans' do
80
+ it 'handles falsey values' do
81
+ expect(Crimp.annotate(false)).to eq([false, 'B'])
82
+ end
83
+
84
+ it 'handles truthy values' do
85
+ expect(Crimp.annotate(true)).to eq([true, 'B'])
86
+ end
87
+ end
88
+
89
+ describe 'Arrays' do
90
+ it 'handles arrays as collection of primitives' do
91
+ expect(Crimp.annotate([1, 2])).to eq([[[1, 'N'], [2, 'N']], 'A'])
92
+ end
93
+
94
+ it 'sorts arrays' do
95
+ expect(Crimp.annotate([2, 1])).to eq([[[1, 'N'], [2, 'N']], 'A'])
96
+ end
97
+
98
+ it 'returns the same signature for two arrays containing the same (unordered) values' do
99
+ arr1 = [1, 2, 3]
100
+ arr2 = [2, 1, 3]
101
+
102
+ expect(Crimp.signature(arr1)).to eq(Crimp.signature(arr2))
103
+ end
104
+
105
+ it 'does not return the same signature for two arrays containing different values' do
106
+ arr1 = [1, 2, 3]
107
+ arr2 = ['1', '2', '3']
108
+
109
+ expect(Crimp.signature(arr1)).to_not eq(Crimp.signature(arr2))
110
+ end
111
+
112
+ it 'sorts an array with mixed strings and symbols' do
113
+ expect(Crimp.notation(["b", :a, "c"])).to eq 'aSbScSA'
114
+ end
115
+ end
116
+
117
+ describe 'Nested Arrays' do
118
+ it 'sorts arrays with a single nested array' do
119
+ expect(Crimp.notation([3, [4, 2], 1])).to eq('1N3N2N4NAA')
120
+ end
121
+
122
+ it 'sorts arrays with a multiple nested arrays' do
123
+ expect(Crimp.notation([3, [4, 2], 1, [6, 5]])).to eq('1N3N2N4NA5N6NAA')
124
+ end
125
+ end
126
+
127
+ describe 'Hashes' do
128
+ it 'handles hashes as collection of primitives' do
129
+ expected = [
130
+ [
131
+ [
132
+ [
133
+ ['a', 'S'],
134
+ ['b', 'S']
135
+ ],
136
+ 'A'
137
+ ]
138
+ ],
139
+ 'H'
140
+ ]
141
+
142
+ expect(Crimp.annotate({a: 'b'})).to eq(expected)
143
+ end
144
+
145
+ it 'sorts hashes by key and then sorts the resulting pair of tuples' do
146
+ expected = [
147
+ [
148
+ [
149
+ [
150
+ [1, 'N'],
151
+ ['e', 'S']
152
+ ],
153
+ 'A'
154
+ ],
155
+ [
156
+ [
157
+ ['a', 'S'],
158
+ ['b', 'S']
159
+ ],
160
+ 'A'
161
+ ],
162
+ [
163
+ [
164
+ ['c', 'S'],
165
+ ['f', 'S']
166
+ ],
167
+ 'A'
168
+ ]
169
+ ],
170
+ 'H'
171
+ ]
172
+
173
+ expect(Crimp.annotate({ a: 'b', f: 'c', 'e' => 1 })).to eq(expected)
174
+ end
175
+
176
+ it 'returns the same signature for two hashes containing the same (unordered) values' do
177
+ hsh1 = { a: 2, b: 1 }
178
+ hsh2 = { b: 1, a: 2 }
179
+
180
+ expect(Crimp.signature(hsh1)).to eq(Crimp.signature(hsh2))
181
+ end
182
+
183
+ it 'does not return the same signature for two hashes containing the different values' do
184
+ hsh1 = { a: 1, b: 2 }
185
+ hsh2 = { a: 2, b: 1 }
186
+
187
+ expect(Crimp.signature(hsh1)).to_not eq(Crimp.signature(hsh2))
188
+ end
189
+
190
+ it 'sorts an hash with mixed key types' do
191
+ expect(Crimp.notation({:b => "c", "d" => "a"})).to eq 'aSdSAbScSAH'
192
+ end
193
+ end
194
+
195
+ describe 'Sets' do
196
+ it 'handles sets as arrays' do
197
+ expect(Crimp.annotate(Set.new([1, 2]))).to eq([[[1, 'N'], [2, 'N']], 'A'])
198
+ end
199
+
200
+ it 'produces the same signature for Array Sets and Arrays' do
201
+ expect(Crimp.signature(Set.new([1, 2]))).to eq(Crimp.signature([2, 1]))
202
+ end
203
+
204
+ it 'handles Hash sets as arrays' do
205
+ expect(Crimp.annotate(Set.new({ 1 => 2 }))).to eq([[[[[1, "N"], [2, "N"]], "A"]], "A"])
206
+ end
207
+
208
+ it 'does NOT produce the same signature for Hash Sets and Hashes' do
209
+ expect(Crimp.signature(Set.new({ 1 => 2 }))).to_not eq(Crimp.signature({ 1 => 2 }))
210
+ end
211
+
212
+ it 'sorts sets as arrays' do
213
+ expect(Crimp.annotate(Set.new([2, 1]))).to eq([[[1, 'N'], [2, 'N']], 'A'])
214
+ end
215
+ end
216
+
217
+ describe 'nested data structures' do
218
+ it 'handles a hash with nested arrays and hashes' do
219
+ obj = { a: [1, 2], b: { c: 'd' } }
220
+
221
+ expected = [
222
+ [
223
+ [
224
+ [
225
+ [
226
+ [
227
+ [1, 'N'],
228
+ [2, 'N']
229
+ ],
230
+ 'A'
231
+ ],
232
+ ['a', 'S']
233
+ ],
234
+ 'A'
235
+ ],
236
+ [
237
+ [
238
+ ['b', 'S'],
239
+ [
240
+ [
241
+ [
242
+ [
243
+ ['c', 'S'],
244
+ ['d', 'S']
245
+ ],
246
+ 'A']
247
+ ],
248
+ 'H']
249
+ ],
250
+ 'A']
251
+ ],
252
+ 'H'
253
+ ]
254
+
255
+ expect(Crimp.annotate(obj)).to eq(expected)
256
+ end
257
+
258
+ it 'handles an array of hashes' do
259
+ obj = [{ a: 1 }, { b: 2 }]
260
+ expected= [
261
+ [
262
+ [
263
+ [
264
+ [
265
+ [
266
+ [1, 'N'],
267
+ ['a', 'S']
268
+ ],
269
+ 'A'
270
+ ]
271
+ ],
272
+ 'H'
273
+ ],
274
+ [
275
+ [
276
+ [
277
+ [
278
+ [2, 'N'],
279
+ ['b', 'S']
280
+ ],
281
+ 'A'
282
+ ]
283
+ ],
284
+ 'H'
285
+ ]
286
+ ],
287
+ 'A'
288
+ ]
289
+
290
+ expect(Crimp.annotate(obj)).to eq(expected)
291
+ end
292
+ end
293
+
294
+ describe 'Objects' do
295
+ it 'raise an error if not in the list of allowed primitives' do
296
+ expect { Crimp.signature(Object.new) }
297
+ .to raise_error(TypeError, 'Expected a (String|Number|Boolean|Nil|Hash|Array), Got Object.')
98
298
  end
99
299
  end
@@ -1,4 +1,3 @@
1
1
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
2
 
3
- require 'pry'
4
3
  require 'crimp'
metadata CHANGED
@@ -1,59 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crimp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BBC News
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-13 00:00:00.000000000 Z
11
+ date: 2018-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.5'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.5'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rake-rspec
57
15
  requirement: !ruby/object:Gem::Requirement
58
16
  requirements:
59
17
  - - ">="
@@ -67,49 +25,7 @@ dependencies:
67
25
  - !ruby/object:Gem::Version
68
26
  version: '0'
69
27
  - !ruby/object:Gem::Dependency
70
- name: rspec-nc
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: guard
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: guard-rspec
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'
111
- - !ruby/object:Gem::Dependency
112
- name: pry
28
+ name: rake
113
29
  requirement: !ruby/object:Gem::Requirement
114
30
  requirements:
115
31
  - - ">="
@@ -123,7 +39,7 @@ dependencies:
123
39
  - !ruby/object:Gem::Version
124
40
  version: '0'
125
41
  - !ruby/object:Gem::Dependency
126
- name: pry-remote
42
+ name: rspec
127
43
  requirement: !ruby/object:Gem::Requirement
128
44
  requirements:
129
45
  - - ">="
@@ -137,22 +53,21 @@ dependencies:
137
53
  - !ruby/object:Gem::Version
138
54
  version: '0'
139
55
  - !ruby/object:Gem::Dependency
140
- name: pry-nav
56
+ name: deepsort
141
57
  requirement: !ruby/object:Gem::Requirement
142
58
  requirements:
143
59
  - - ">="
144
60
  - !ruby/object:Gem::Version
145
61
  version: '0'
146
- type: :development
62
+ type: :runtime
147
63
  prerelease: false
148
64
  version_requirements: !ruby/object:Gem::Requirement
149
65
  requirements:
150
66
  - - ">="
151
67
  - !ruby/object:Gem::Version
152
68
  version: '0'
153
- description: |
154
- Shamelessly lifted from http://stackoverflow.com/questions/6461812/creating-an-md5-hash-of-a-number-string-array-or-hash-in-ruby
155
- All credit should go to http://stackoverflow.com/users/394282/luke
69
+ description: Platform agnostic MD5 hash from simple data structures made of numbers,
70
+ strings, booleans, nil, arrays or hashes.
156
71
  email:
157
72
  - D&ENewsFrameworksTeam@bbc.co.uk
158
73
  executables: []
@@ -163,17 +78,16 @@ files:
163
78
  - ".ruby-version"
164
79
  - ".travis.yml"
165
80
  - Gemfile
166
- - Guardfile
167
81
  - LICENSE.txt
168
82
  - README.md
169
83
  - Rakefile
170
84
  - crimp.gemspec
171
85
  - lib/crimp.rb
172
- - lib/crimp/version.rb
173
- - spec/consistency_spec.rb
86
+ - spec/acceptance_data.yml
87
+ - spec/acceptance_spec.rb
174
88
  - spec/crimp_spec.rb
175
89
  - spec/spec_helper.rb
176
- homepage: ''
90
+ homepage: https://www.bbc.co.uk/news
177
91
  licenses:
178
92
  - MIT
179
93
  metadata: {}
@@ -196,8 +110,9 @@ rubyforge_project:
196
110
  rubygems_version: 2.7.6
197
111
  signing_key:
198
112
  specification_version: 4
199
- summary: Creating an md5 hash of a number, string, array, or hash in Ruby
113
+ summary: Creates an MD5 hash from simple data structures.
200
114
  test_files:
201
- - spec/consistency_spec.rb
115
+ - spec/acceptance_data.yml
116
+ - spec/acceptance_spec.rb
202
117
  - spec/crimp_spec.rb
203
118
  - spec/spec_helper.rb
data/Guardfile DELETED
@@ -1,6 +0,0 @@
1
- guard 'rspec' do
2
- watch(%r{^spec/.+_spec\.rb$})
3
- watch(%r{^lib/.+\.rb$})
4
- watch(%r{^spec/support/(.+)\.rb$}) { 'spec' }
5
- watch('spec/spec_helper.rb') { 'spec' }
6
- end
@@ -1,3 +0,0 @@
1
- module Crimp
2
- VERSION = '0.2.0'.freeze
3
- end
@@ -1,44 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Crimp do
4
- describe '.signature' do
5
- context 'check MD5 consistent across versions' do
6
- context 'given a Hash' do
7
- let(:hash) do
8
- {
9
- a: 123,
10
- b: 1.23,
11
- c: 'string',
12
- d: :sym
13
- }
14
- end
15
- let(:md5) { '1dd744d51279187cc08cabe240f98be2' }
16
-
17
- specify { expect(subject.signature(hash)).to eq md5 }
18
- end
19
-
20
- context 'given legacy Hash' do
21
- let(:hash) do
22
- { a: { b: 'b', c: 'c' }, d: 'd' }
23
- end
24
- let(:md5) { '68d07febc4f47f56fa6ef5de063a77b1' }
25
-
26
- specify { expect(subject.signature(hash)).to eq md5 }
27
- end
28
-
29
- context 'given an Array' do
30
- let(:array) { [123, 1.23, 'string', :sym] }
31
- let(:md5) { 'cd29980f258eef3faceca4f4da02ec65' }
32
-
33
- specify { expect(subject.signature(array)).to eq md5 }
34
- end
35
-
36
- context 'given legacy Array' do
37
- let(:array) { [1, 2, 3, [4, [5, 6]]] }
38
- let(:md5) { '4dc4e1161c9315db0bc43c0201b3ec05' }
39
-
40
- specify { expect(subject.signature(array)).to eq md5 }
41
- end
42
- end
43
- end
44
- end