Skip to content

Releases: kylekthompson/strict

v1.5.0

12 Apr 04:05
7ac2719
Compare
Choose a tag to compare

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

Full Changelog: v1.4.0...v1.5.0

v1.4.0

02 Nov 12:18
67de03a
Compare
Choose a tag to compare

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

Full Changelog: v1.3.1...v1.4.0

v1.3.1

20 Oct 17:39
c1129b5
Compare
Choose a tag to compare

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

Full Changelog: v1.3.0...v1.3.1

v1.3.0

18 Oct 19:00
1d72f84
Compare
Choose a tag to compare

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

Full Changelog: v1.2.0...v1.3.0

v1.2.0

14 Oct 16:41
f2c8dad
Compare
Choose a tag to compare

Support back to Ruby 3.0.0

What's Changed

New Contributors

Full Changelog: v1.1.0...v1.2.0

v1.1.0

14 Oct 16:06
e24dda6
Compare
Choose a tag to compare

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

Full Changelog: v1.0.0...v1.1.0

v1.0.0

12 Oct 05:40
f8a84d6
Compare
Choose a tag to compare

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