blood_contracts 0.2.1 → 1.0.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: 28ac2afbaaff105357ceefb2e96e4812f5689ceca47fecc4eeffd5c51c45d3a9
4
- data.tar.gz: 3d14390aa3ea943b3ad53495b3855c673d2c87da925b3230071d22c555de506c
3
+ metadata.gz: d2c47ef6538d2aa19e88887ae50e12dd59518fa31077b75dbdf8c5d143cea7a2
4
+ data.tar.gz: 1bc58ec7ce70d3490454a123cf10dcdee4f97b9a94dc1129a1177319ec11d02a
5
5
  SHA512:
6
- metadata.gz: 8850f7821e11d0b3ed3acf837569b7ada9b45f0844fb8491a9e212bda1e1599035635f849e28acb0f7966933fe5cb0e4d865266012ce0148a37a2d6afe3679e9
7
- data.tar.gz: 8e7e070351a3dc36d63bf6f0817176489350c0ea361dc3deb8cc48f5e578909bb2b62379e421d82b3d27267c9892d5965f788648a64f8b0bb86db10421d37ffc
6
+ metadata.gz: 6b8a1a15937a6b35cdce7223d15efad1c5bc06edcb1eaabfcc34d0cf22ccceeb273ba0b6ed8da2c92ba7896fc0dc33fc20840a8c4f8f9b0f1329c2c9086d732c
7
+ data.tar.gz: a82397121b167e929cae791fcfefccf6aa90509606ca7836ca9640b580720a993ee4cd046c23e74a688540447878bf4a7b4ee8fb6e3f51439e7b8640c2fcf9ab
data/README.md CHANGED
@@ -1,14 +1,68 @@
1
+ [adt_wiki]: https://en.wikipedia.org/wiki/Algebraic_data_type
2
+ [functional_programming_wiki]: https://en.wikipedia.org/wiki/Functional_programming
3
+ [refinement_types_wiki]: https://en.wikipedia.org/wiki/Refinement_type
4
+ [ebaymag]: https://ebaymag.com/
5
+
1
6
  # BloodContracts
2
7
 
3
- Ruby gem to define and validate behavior of API using contract.
8
+ Simple and agile Ruby data validation tool inspired by refinement types and functional approach
9
+
10
+ * **Powerful**. [Algebraic Data Type][adt_wiki] guarantees that gem is enough to implement any kind of complex data validation, while [Functional Approach][functional_programming_wiki] gives you full control over validation outcomes
11
+ * **Simple**. You could write your first [Refinment Type][refinement_types_wiki] as simple as single Ruby method in single class
12
+ * **Brings transparency**. Comes with instrumentation tools, so now you will exactly know how often each type matches in your production
13
+ * **Rubyish**. DSL is inspired by Ruby Struct. If you love Ruby way you'd like the BloodContracts types
14
+ * **Born in production**. Created on basis of [eBaymag][ebaymag] project, used as a tool to control and monitor data inside API communication
4
15
 
5
- Possible use-cases:
6
- - Automated external API status check (shooting with critical requests and validation that behavior meets the contract);
7
- - Automated detection of unexpected external API behavior (Rack::request/response pairs that don't match contract);
8
- - Contract definition assistance tool (generate real-a-like requests and iterate through oddities of your system behavior)
16
+ ```ruby
17
+ # Write your "types" as simple as...
18
+ class Email < ::BC::Refined
19
+ REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
9
20
 
10
- <a href="https://evilmartians.com/">
11
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
21
+ def match
22
+ return if (context[:email] = value.to_s) =~ REGEX
23
+ failure(:invalid_email)
24
+ end
25
+ end
26
+
27
+ class Phone < ::BC::Refined
28
+ REGEX = /\A(\+7|8)(9|8)\d{9}\z/i
29
+
30
+ def match
31
+ return if (context[:phone] = value.to_s) =~ REGEX
32
+ failure(:invalid_phone)
33
+ end
34
+ end
35
+
36
+ # ... compose them...
37
+ Login = Email.or_a(Phone)
38
+
39
+ # ... and match!
40
+ case match = Login.match("not-a-login")
41
+ when Phone, Email
42
+ match # use as you wish, you exactly know what kind of login you received
43
+ when BC::ContractFailure # translate error message
44
+ match.messages # => [:no_matches, :invalid_phone, :invalid_email]
45
+ else raise # to make sure you covered all scenarios (Functional Way)
46
+ end
47
+
48
+ # And then in
49
+ # config/initializers/contracts.rb
50
+
51
+ module Contracts
52
+ class YabedaInstrument
53
+ def call(session)
54
+ valid_marker = session.valid? ? "V" : "I"
55
+ result = "[#{valid_marker}] #{session.result_type_name}"
56
+ Yabeda.api_contract_matches.increment(result: result)
57
+ end
58
+ end
59
+ end
60
+
61
+ BloodContracts::Instrumentation.configure do |cfg|
62
+ # Attach to every BC::Refined ancestor with Login in the name
63
+ cfg.instrument "Login", Contracts::YabedaInstrument.new
64
+ end
65
+ ```
12
66
 
13
67
  ## Installation
14
68
 
@@ -28,120 +82,10 @@ Or install it yourself as:
28
82
 
29
83
  ## Usage
30
84
 
31
- ```ruby
32
- # define contract
33
- def contract
34
- Hash[
35
- success: {
36
- check: ->(_input, output) do
37
- data = output.data
38
- shipping_cost = data.dig(
39
- "BkgDetails", "QtdShp", "ShippingCharge"
40
- )
41
- output.success? && shipping_cost.present?
42
- end,
43
- threshold: 0.98,
44
- },
45
- data_missing_error: {
46
- check: ->(_input, output) do
47
- output.error_codes.present? &&
48
- (output.error_codes - ["111"]).empty?
49
- end,
50
- limit: 0.01,
51
- },
52
- data_invalid_error: {
53
- check: ->(_input, output) do
54
- output.error_codes.present? &&
55
- (output.error_codes - ["4300", "123454"]).empty?
56
- end,
57
- limit: 0.01,
58
- },
59
- strange_weight: {
60
- check: ->(input, output) do
61
- input.weight > 100 && output.error_codes.empty? && !output.success?
62
- end,
63
- limit: 0.01,
64
- }
65
- ]
66
- end
67
-
68
- # define the API input
69
- def generate_data
70
- DHL::RequestData.new(
71
- data_source.origin_addresses.sample,
72
- data_source.destinations.sample,
73
- data_source.prices.sample,
74
- data_source.products.sample,
75
- data_source.weights.sample,
76
- data_source.dates.sample.days.since.to_date.to_s(:iso8601),
77
- data_source.accounts.sample,
78
- ).to_h
79
- end
80
-
81
- def data_source
82
- Hashie::Mash.new(load_fixture("dhl/obfuscated-production-data.yaml"))
83
- end
84
-
85
- # initiate contract suite
86
- # with default storage (in tmp/blood_contracts/ folder of the project)
87
- contract_suite = BloodContract::Suite.new(
88
- contract: contract,
89
- data_generator: method(:generate_data),
90
- )
91
-
92
- # with custom storage backend (e.g. Postgres DB)
93
- conn = PG.connect( dbname: "blood_contracts" )
94
- conn.exec(<<~SQL);
95
- CREATE TABLE runs (
96
- created_at timestamp DEFAULT current_timestamp,
97
- contract_name text,
98
- rules_matched array text[],
99
- input text,
100
- output text
101
- );
102
- SQL
103
-
104
- contract_suite = BloodContract::Suite.new(
105
- contract: contract,
106
- data_generator: method(:generate_data),
107
-
108
- storage_backend: ->(contract_name, rules_matched, input, output) do
109
- conn.exec(<<~SQL, contract_name, rules_matched, input, output)
110
- INSERT INTO runs (contract_name, rules_matched, input, output) VALUES (?, ?, ?, ?);
111
- SQL
112
- end
113
- )
114
-
115
- # run validation
116
- runner = BloodContract::Runner.new(
117
- ->(input) { DHL::Client.call(input) }
118
- suite: contract_suite,
119
- time_to_run: 3600, # seconds
120
- # or
121
- # iterations: 1000
122
- ).tap(&:call)
123
-
124
- # chech the results
125
- runner.valid? # true if behavior was aligned with contract or false in any other case
126
- runner.run_stats # stats about each contract rule or exceptions occasions during the run
127
-
128
- ```
129
-
130
- ## TODO
131
- - Add rake task to run contracts validation
132
- - Add executable to run contracts validation
133
-
134
- ## Possible Features
135
- - Store the actual code of the contract rules in Storage (gem 'sourcify')
136
- - Store reports in Storage
137
- - Export/import contracts to YAML, JSON....
138
- - Contracts inheritance (already exists using `Hash#merge`?)
139
- - Export `Runner#run_stats` to CSV
140
- - Create simple web app, to read the reports
85
+ This gem is just facade for the whole data validation and monitoring toolset.
141
86
 
142
- ## Other specific use cases
87
+ For deeper understanding see [BloodContracts::Core](https://github.com/sclinede/blood_contracts-core), [BloodContracts::Ext](https://github.com/sclinede/blood_contracts-ext) and [BloodContracts::Instrumentation](https://github.com/sclinede/blood_contracts-instrumentation)
143
88
 
144
- For Rack request/response validation use: `blood_contracts-rack`
145
89
 
146
90
  ## Development
147
91
 
data/Rakefile CHANGED
@@ -4,3 +4,33 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task default: :spec
7
+ task "db:prepare" do
8
+ require "dotenv"
9
+ Dotenv.load(".env.development")
10
+ sh "createdb #{env_database_name}" do
11
+ # Ignore errors
12
+ end
13
+
14
+ Dotenv.overload(".env.test")
15
+ sh "createdb #{env_database_name}" do
16
+ # Ignore errors
17
+ end
18
+ end
19
+
20
+ task "db:drop" do
21
+ require "dotenv"
22
+ Dotenv.load(".env.development")
23
+ sh "dropdb #{env_database_name}" do
24
+ # Ignore errors
25
+ end
26
+
27
+ Dotenv.overload(".env.test")
28
+ sh "dropdb #{env_database_name}" do
29
+ # Ignore errors
30
+ end
31
+ end
32
+
33
+ def env_database_name
34
+ require "uri"
35
+ ENV.fetch("DATABASE_URL").split("/").last
36
+ end
@@ -3,9 +3,10 @@
3
3
  require "bundler/setup"
4
4
  require "blood_contracts"
5
5
 
6
+
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
8
9
 
9
10
  # (If you use this, don't forget to add pry to your Gemfile!)
10
- require "pry"
11
- Pry.start
11
+ require "irb"
12
+ IRB.start
@@ -1,37 +1,20 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "blood_contracts/version"
5
-
6
1
  Gem::Specification.new do |spec|
7
2
  spec.name = "blood_contracts"
8
3
 
9
- spec.version = BloodContracts::VERSION
10
- spec.authors = ["Sergey Dolganov"]
11
- spec.email = ["dolganov@evl.ms"]
4
+ spec.version = "1.0.0"
5
+ spec.authors = ["Sergey Dolganov (sclinede)"]
6
+ spec.email = ["sclinede@evilmartians.com"]
12
7
 
13
- spec.summary = "Ruby gem to define and validate behavior of API using contracts."
14
- spec.description = "Ruby gem to define and validate behavior of API using contracts."
8
+ spec.summary = " Ruby gem for runtime data validation and monitoring using the contracts approach."
9
+ spec.description = " Ruby gem for runtime data validation and monitoring using the contracts approach."
15
10
  spec.homepage = "https://github.com/sclinede/blood_contracts"
16
11
  spec.license = "MIT"
17
12
 
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
20
- end
21
-
22
- # Will be introduced soon
23
- # spec.bindir = "exe"
24
- # spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
- spec.require_paths = ["lib"]
26
-
27
- spec.required_ruby_version = '>= 2.2.0'
13
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
28
14
 
29
- spec.add_runtime_dependency "dry-initializer", "~> 2.0"
30
- spec.add_runtime_dependency "nanoid", "~> 0.2"
31
- spec.add_runtime_dependency "hashie", "~> 3.0"
15
+ spec.required_ruby_version = ">= 2.4"
32
16
 
33
- spec.add_development_dependency "bundler", "~> 1.16"
34
- spec.add_development_dependency "rake", "~> 10.0"
35
- spec.add_development_dependency "rspec", "~> 3.0"
36
- spec.add_development_dependency "pry", "~> 0.9"
17
+ spec.add_runtime_dependency "blood_contracts-core", "~> 0.4"
18
+ spec.add_runtime_dependency "blood_contracts-ext", "~> 0.1"
19
+ spec.add_runtime_dependency "blood_contracts-instrumentation", "~> 0.1"
37
20
  end
@@ -1,38 +1,3 @@
1
- require "blood_contracts/version"
2
-
3
- require_relative "extensions/string.rb"
4
- require "dry-initializer"
5
- require "hashie/mash"
6
-
7
- require_relative "blood_contracts/suite"
8
- require_relative "blood_contracts/storage"
9
- require_relative "blood_contracts/runner"
10
- require_relative "blood_contracts/debugger"
11
- require_relative "blood_contracts/base_contract"
12
-
13
- module BloodContracts
14
- def run_name
15
- @__contracts_run_name
16
- end
17
- module_function :run_name
18
-
19
- def run_name=(run_name)
20
- @__contracts_run_name = run_name
21
- end
22
- module_function :run_name=
23
-
24
- if defined?(RSpec) && RSpec.respond_to?(:configure)
25
- require_relative "rspec/meet_contract_matcher"
26
-
27
- RSpec.configure do |config|
28
- config.include ::RSpec::MeetContractMatcher
29
- config.filter_run_excluding contract: true
30
- config.before(:suite) do
31
- BloodContracts.run_name = ::Nanoid.generate(size: 10)
32
- end
33
- config.define_derived_metadata(file_path: %r{/spec/contracts/}) do |meta|
34
- meta[:contract] = true
35
- end
36
- end
37
- end
38
- end
1
+ require "blood_contracts/core"
2
+ require "blood_contracts/ext"
3
+ require "blood_contracts/instrumentation"
@@ -0,0 +1,36 @@
1
+ RSpec.describe BloodContracts do
2
+ describe ".run_name" do
3
+ before { BloodContracts.run_name = "External run name" }
4
+
5
+ it { expect(BloodContracts.run_name).to eq("External run name") }
6
+ end
7
+
8
+ describe ".storage" do
9
+ context "when default configuration" do
10
+ let(:expected_backend) { BloodContracts::Storages::FileBackend }
11
+ it "has assigned storage" do
12
+ expect(BloodContracts.storage.backend).to be_kind_of(expected_backend)
13
+ end
14
+ end
15
+
16
+ context "when custom storage configured" do
17
+ before do
18
+ BloodContracts.config { |config| config.storage[:type] = :postgres }
19
+ end
20
+ let(:expected_backend) { BloodContracts::Storages::PostgresBackend }
21
+
22
+ it "has assigned custom storage" do
23
+ expect(BloodContracts.storage.backend).to be_kind_of(expected_backend)
24
+ end
25
+ end
26
+ end
27
+
28
+ context "when custom storage configured" do
29
+ end
30
+
31
+ context "when custom sampling configured" do
32
+ end
33
+
34
+ context "when RSpec is defined" do
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ require "bundler/setup"
2
+ require "blood_contracts"
3
+ require "dotenv"
4
+ Dotenv.load(".env.test")
5
+
6
+ RSpec.configure do |config|
7
+ # Enable flags like --only-failures and --next-failure
8
+ config.example_status_persistence_file_path = ".rspec_status"
9
+
10
+ # Disable RSpec exposing methods globally on `Module` and `main`
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end
metadata CHANGED
@@ -1,122 +1,66 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blood_contracts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - Sergey Dolganov
7
+ - Sergey Dolganov (sclinede)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-07 00:00:00.000000000 Z
11
+ date: 2019-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: dry-initializer
14
+ name: blood_contracts-core
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '0.4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
26
+ version: '0.4'
27
27
  - !ruby/object:Gem::Dependency
28
- name: nanoid
28
+ name: blood_contracts-ext
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.2'
33
+ version: '0.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.2'
40
+ version: '0.1'
41
41
  - !ruby/object:Gem::Dependency
42
- name: hashie
42
+ name: blood_contracts-instrumentation
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
47
+ version: '0.1'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: bundler
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.16'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.16'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '10.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '10.0'
83
- - !ruby/object:Gem::Dependency
84
- name: rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '3.0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '3.0'
97
- - !ruby/object:Gem::Dependency
98
- name: pry
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '0.9'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '0.9'
111
- description: Ruby gem to define and validate behavior of API using contracts.
54
+ version: '0.1'
55
+ description: " Ruby gem for runtime data validation and monitoring using the contracts
56
+ approach."
112
57
  email:
113
- - dolganov@evl.ms
58
+ - sclinede@evilmartians.com
114
59
  executables: []
115
60
  extensions: []
116
61
  extra_rdoc_files: []
117
62
  files:
118
63
  - ".gitignore"
119
- - ".travis.yml"
120
64
  - CODE_OF_CONDUCT.md
121
65
  - Gemfile
122
66
  - LICENSE.txt
@@ -126,22 +70,8 @@ files:
126
70
  - bin/setup
127
71
  - blood_contracts.gemspec
128
72
  - lib/blood_contracts.rb
129
- - lib/blood_contracts/base_contract.rb
130
- - lib/blood_contracts/contracts/description.rb
131
- - lib/blood_contracts/contracts/iterator.rb
132
- - lib/blood_contracts/contracts/matcher.rb
133
- - lib/blood_contracts/contracts/statistics.rb
134
- - lib/blood_contracts/contracts/validator.rb
135
- - lib/blood_contracts/debugger.rb
136
- - lib/blood_contracts/runner.rb
137
- - lib/blood_contracts/storage.rb
138
- - lib/blood_contracts/storages/base_backend.rb
139
- - lib/blood_contracts/storages/file_backend.rb
140
- - lib/blood_contracts/storages/serializer.rb
141
- - lib/blood_contracts/suite.rb
142
- - lib/blood_contracts/version.rb
143
- - lib/extensions/string.rb
144
- - lib/rspec/meet_contract_matcher.rb
73
+ - spec/blood_contracts_spec.rb
74
+ - spec/spec_helper.rb
145
75
  homepage: https://github.com/sclinede/blood_contracts
146
76
  licenses:
147
77
  - MIT
@@ -154,16 +84,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
154
84
  requirements:
155
85
  - - ">="
156
86
  - !ruby/object:Gem::Version
157
- version: 2.2.0
87
+ version: '2.4'
158
88
  required_rubygems_version: !ruby/object:Gem::Requirement
159
89
  requirements:
160
90
  - - ">="
161
91
  - !ruby/object:Gem::Version
162
92
  version: '0'
163
93
  requirements: []
164
- rubyforge_project:
165
- rubygems_version: 2.7.6
94
+ rubygems_version: 3.0.3
166
95
  signing_key:
167
96
  specification_version: 4
168
- summary: Ruby gem to define and validate behavior of API using contracts.
97
+ summary: Ruby gem for runtime data validation and monitoring using the contracts approach.
169
98
  test_files: []