blood_contracts 0.2.1 → 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: 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: []