Releases: kylekthompson/strict
v1.5.0
Support sampling of validation to make Strict more Lenient
This release exposes configuration options to change the sample rate for validating attributes, parameters, and return values. This new capability means that you can validate all the time in your tests and only 10% of the time in production, for example. Additionally, you can apply different sample rates in performance sensitive areas of code if you need to disable validation entirely:
# Globally
Strict.configure do |c|
c.sample_rate = 0.75 # run validation ~75% of the time
end
Strict.configure do |c|
c.sample_rate = 0 # disable validation (Strict becomes Lenient)
end
Strict.configure do |c|
c.sample_rate = 0 # always run validation
end
# Locally within the block (only applies to the current thread)
Strict.with_overrides(sample_rate: 0) do
# Use Strict as you normally would
Strict.with_overrides(sample_rate: 0.5) do
# Overrides can be nested
end
end
What's Changed
- Bundle update by @kylekthompson in #58
- Expose a configuration for Strict by @kylekthompson in #59
- Make with_overrides thread-safe by @kylekthompson in #60
- Sample validity of attributes, parameters, and returns based on the configuration by @kylekthompson in #61
Full Changelog: v1.4.0...v1.5.0
v1.4.0
Permit implementations to splat their parameters if unused
When writing mocks and passing those to an implementation, often the mocking library will define the methods as def foo(*args, **kwargs, &block)
such that they can inspect the arguments and assert upon them later. With this change, parameters that are not directly used by the implemented method can be splatted. When calling the implementation through the interface, types of course will still be strictly checked as they are today. This loosens the strictness of Strict::Interface
slightly in the regard that the method does not have to be an exact match, but rather must be able to be safely called by the interface instance.
What's Changed
- Bump debug from 1.6.2 to 1.6.3 by @dependabot in #28
- Bundle Update by @kylekthompson in #31
- Permit implementations to splat their arguments if unused by @kylekthompson in #32
Full Changelog: v1.3.1...v1.4.0
v1.3.1
Interfaces support parameterless methods
@tonywok observed a bug with Strict::Interface
where exposing parameterless methods led to a syntax error. This has been corrected.
What's Changed
- Interfaces now support parameterless methods by @kylekthompson in #27
Full Changelog: v1.3.0...v1.3.1
v1.3.0
Interface Coercers
Strict Interface Coercers
You can now easily coerce into interfaces defined with Strict::Interface
. MyInterface.coercer
will provide a coercer that will attempt to coercer a passed-in implementation into your interface (and strictly validate it along the way, of course).
class Storage
extend Strict::Interface
expose(:write) do
key String
contents String
returns Boolean()
end
expose(:read) do
key String
returns AnyOf(String, nil)
end
end
module Storages
class Memory
def initialize
@storage = {}
end
def write(key:, contents:)
storage[key] = contents
true
end
def read(key:)
storage[key]
end
private
attr_reader :storage
end
end
class Writer
include Strict::Object
attributes do
storage Storage, coerce: Storage.coercer
end
end
writer = Writer.new(storage: Storages::Memory.new)
# => #<Writer storage=#<Storage implementation=#<Storages::Memory>>>
What's Changed
- Expose an interface coercer by @kylekthompson in #26
Full Changelog: v1.2.0...v1.3.0
v1.2.0
Support back to Ruby 3.0.0
What's Changed
- Support back to Ruby 3.0.0 by @kylekthompson in #22
- Bump debug from 1.6.1 to 1.6.2 by @dependabot in #23
- Bump minitest from 5.16.2 to 5.16.3 by @dependabot in #24
New Contributors
- @dependabot made their first contribution in #23
Full Changelog: v1.1.0...v1.2.0
v1.1.0
Strictly Define Interfaces (and more!)
Strict::Interface
With this release, we've introduced Strict::Interface
- a new way to strictly validate a cohesive set of functionality that might have different implementations. Consider, for example, a codebase that offers a Storage
class:
class Storage
def write(key:, contents:)
# write to file
end
def read(key:)
# read from file
end
end
Within your tests, rather than writing to the filesystem you might want to write to an in-memory store. Where you typically might rely on duck typing, you can now define a Strict::Interface
and multiple implementations which conform to that interface. See below for an example:
class Storage
extend Strict::Interface
expose(:write) do
key String
contents String
returns Boolean()
end
expose(:read) do
key String
returns AnyOf(String, nil)
end
end
module Storages
class Memory
def initialize
@storage = {}
end
def write(key:, contents:)
storage[key] = contents
true
end
def read(key:)
storage[key]
end
private
attr_reader :storage
end
end
storage = Storage.new(Storages::Memory.new)
# => #<Storage implementation=#<Storages::Memory>>
storage.write(key: "some/path/to/file.rb", contents: "Hello")
# => true
storage.write(key: "some/path/to/file.rb", contents: {})
# => Strict::MethodCallError
storage.read(key: "some/path/to/file.rb")
# => "Hello"
storage.read(key: "some/path/to/other.rb")
# => nil
module Storages
class Wat
def write(key:)
end
end
end
storage = Storage.new(Storages::Wat.new)
# => Strict::ImplementationDoesNotConformError
Strict Attributes Coercers
You can now easily coerce into objects or values defined with strict attributes. ValueOrObjectClass.coercer
will provide a coercer that will turn a hash-like object into an instance of the class.
class Money
include Strict::Value
attributes do
amount_in_cents Integer
currency AnyOf("USD", "CAD"), default: "USD"
end
end
class Transaction
include Strict::Value
attributes do
from_account String
to_account String
amount Money, coerce: Money.coercer
end
end
Transaction.new(from_account: "1", to_account: "2", amount: { amount_in_cents: 100_00 })
# => #<Transaction from_account="1" to_account="2" amount=#<Money amount_in_cents=100_00 currency="USD">>
What's Changed
- Make some stronger Strict::Method assertions by @kylekthompson in #15
- Add coercers for attributes classes, hashes, and arrays by @kylekthompson in #17
- Add Strict::Interface by @kylekthompson in #19
- Add Strict::Interface examples by @kylekthompson in #20
- v1.1.0 by @kylekthompson in #21
Full Changelog: v1.0.0...v1.1.0
v1.0.0
Strict's Initial Release!
Strict provides a means to strictly validate instantiation of values, instantiation and attribute assignment of objects, and method calls at runtime.
Some example usage can be seen in the README or below:
Strict::Value
class Money
include Strict::Value
attributes do
amount_in_cents Integer
currency AnyOf("USD", "CAD"), default: "USD"
end
end
Money.new(amount_in_cents: 100_00)
# => #<Money amount_in_cents=100_00 currency="USD">
Money.new(amount_in_cents: 100_00, currency: "CAD")
# => #<Money amount_in_cents=100_00 currency="CAD">
Money.new(amount_in_cents: 100.00)
# => Strict::InitializationError
Money.new(amount_in_cents: 100_00).with(amount_in_cents: 200_00)
# => #<Money amount_in_cents=200_00 currency="USD">
Money.new(amount_in_cents: 100_00).amount_in_cents = 50_00
# => NoMethodError
Money.new(amount_in_cents: 100_00) == Money.new(amount_in_cents: 100_00)
# => true
Strict::Object
class Stateful
include Strict::Object
attributes do
some_state String
dependency Anything(), default: nil
end
end
Stateful.new(some_state: "123")
# => #<Stateful some_state="123" dependency=nil>
Stateful.new(some_state: "123").with(some_state: "456")
# => NoMethodError
Stateful.new(some_state: "123").some_state = "456"
# => "456"
# => #<Stateful some_state="456" dependency=nil>
Stateful.new(some_state: "123").some_state = 456
# => Strict::AssignmentError
Stateful.new(some_state: "123") == Stateful.new(some_state: "123")
# => false
Strict::Method
class UpdateEmail
extend Strict::Method
sig do
user_id String, coerce: ->(value) { value.to_s }
email String
returns AnyOf(true, nil)
end
def call(user_id:, email:)
# contrived logic
user_id == email
end
end
UpdateEmail.new.call(user_id: 123, email: "123")
# => true
UpdateEmail.new.call(user_id: "123", email: "123")
# => true
UpdateEmail.new.call(user_id: "123", email: 123)
# => Strict::MethodCallError
UpdateEmail.new.call(user_id: "123", email: "456")
# => Strict::MethodReturnError