README.md in blood_contracts-0.2.1 vs README.md in blood_contracts-1.0.0

- old
+ new

@@ -1,148 +1,92 @@ +[adt_wiki]: https://en.wikipedia.org/wiki/Algebraic_data_type +[functional_programming_wiki]: https://en.wikipedia.org/wiki/Functional_programming +[refinement_types_wiki]: https://en.wikipedia.org/wiki/Refinement_type +[ebaymag]: https://ebaymag.com/ + # BloodContracts -Ruby gem to define and validate behavior of API using contract. +Simple and agile Ruby data validation tool inspired by refinement types and functional approach -Possible use-cases: -- Automated external API status check (shooting with critical requests and validation that behavior meets the contract); -- Automated detection of unexpected external API behavior (Rack::request/response pairs that don't match contract); -- Contract definition assistance tool (generate real-a-like requests and iterate through oddities of your system behavior) +* **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 +* **Simple**. You could write your first [Refinment Type][refinement_types_wiki] as simple as single Ruby method in single class +* **Brings transparency**. Comes with instrumentation tools, so now you will exactly know how often each type matches in your production +* **Rubyish**. DSL is inspired by Ruby Struct. If you love Ruby way you'd like the BloodContracts types +* **Born in production**. Created on basis of [eBaymag][ebaymag] project, used as a tool to control and monitor data inside API communication -<a href="https://evilmartians.com/"> -<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a> - -## Installation - -Add this line to your application's Gemfile: - ```ruby -gem 'blood_contracts' -``` +# Write your "types" as simple as... +class Email < ::BC::Refined + REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i -And then execute: + def match + return if (context[:email] = value.to_s) =~ REGEX + failure(:invalid_email) + end +end - $ bundle +class Phone < ::BC::Refined + REGEX = /\A(\+7|8)(9|8)\d{9}\z/i -Or install it yourself as: + def match + return if (context[:phone] = value.to_s) =~ REGEX + failure(:invalid_phone) + end +end - $ gem install blood_contracts +# ... compose them... +Login = Email.or_a(Phone) -## Usage - -```ruby -# define contract -def contract - Hash[ - success: { - check: ->(_input, output) do - data = output.data - shipping_cost = data.dig( - "BkgDetails", "QtdShp", "ShippingCharge" - ) - output.success? && shipping_cost.present? - end, - threshold: 0.98, - }, - data_missing_error: { - check: ->(_input, output) do - output.error_codes.present? && - (output.error_codes - ["111"]).empty? - end, - limit: 0.01, - }, - data_invalid_error: { - check: ->(_input, output) do - output.error_codes.present? && - (output.error_codes - ["4300", "123454"]).empty? - end, - limit: 0.01, - }, - strange_weight: { - check: ->(input, output) do - input.weight > 100 && output.error_codes.empty? && !output.success? - end, - limit: 0.01, - } - ] +# ... and match! +case match = Login.match("not-a-login") +when Phone, Email + match # use as you wish, you exactly know what kind of login you received +when BC::ContractFailure # translate error message + match.messages # => [:no_matches, :invalid_phone, :invalid_email] +else raise # to make sure you covered all scenarios (Functional Way) end -# define the API input -def generate_data - DHL::RequestData.new( - data_source.origin_addresses.sample, - data_source.destinations.sample, - data_source.prices.sample, - data_source.products.sample, - data_source.weights.sample, - data_source.dates.sample.days.since.to_date.to_s(:iso8601), - data_source.accounts.sample, - ).to_h +# And then in +# config/initializers/contracts.rb + +module Contracts + class YabedaInstrument + def call(session) + valid_marker = session.valid? ? "V" : "I" + result = "[#{valid_marker}] #{session.result_type_name}" + Yabeda.api_contract_matches.increment(result: result) + end + end end -def data_source - Hashie::Mash.new(load_fixture("dhl/obfuscated-production-data.yaml")) +BloodContracts::Instrumentation.configure do |cfg| + # Attach to every BC::Refined ancestor with Login in the name + cfg.instrument "Login", Contracts::YabedaInstrument.new end +``` -# initiate contract suite -# with default storage (in tmp/blood_contracts/ folder of the project) -contract_suite = BloodContract::Suite.new( - contract: contract, - data_generator: method(:generate_data), -) +## Installation -# with custom storage backend (e.g. Postgres DB) -conn = PG.connect( dbname: "blood_contracts" ) -conn.exec(<<~SQL); - CREATE TABLE runs ( - created_at timestamp DEFAULT current_timestamp, - contract_name text, - rules_matched array text[], - input text, - output text - ); -SQL +Add this line to your application's Gemfile: -contract_suite = BloodContract::Suite.new( - contract: contract, - data_generator: method(:generate_data), +```ruby +gem 'blood_contracts' +``` - storage_backend: ->(contract_name, rules_matched, input, output) do - conn.exec(<<~SQL, contract_name, rules_matched, input, output) - INSERT INTO runs (contract_name, rules_matched, input, output) VALUES (?, ?, ?, ?); - SQL - end -) +And then execute: -# run validation -runner = BloodContract::Runner.new( - ->(input) { DHL::Client.call(input) } - suite: contract_suite, - time_to_run: 3600, # seconds - # or - # iterations: 1000 - ).tap(&:call) + $ bundle -# chech the results -runner.valid? # true if behavior was aligned with contract or false in any other case -runner.run_stats # stats about each contract rule or exceptions occasions during the run +Or install it yourself as: -``` + $ gem install blood_contracts -## TODO -- Add rake task to run contracts validation -- Add executable to run contracts validation +## Usage -## Possible Features -- Store the actual code of the contract rules in Storage (gem 'sourcify') -- Store reports in Storage -- Export/import contracts to YAML, JSON.... -- Contracts inheritance (already exists using `Hash#merge`?) -- Export `Runner#run_stats` to CSV -- Create simple web app, to read the reports +This gem is just facade for the whole data validation and monitoring toolset. -## Other specific use cases +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) -For Rack request/response validation use: `blood_contracts-rack` ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.