Farce / dry-types
The opt-in dry-types integration validates input and constructs Farce values from the result. It is useful when parsed input is headed to concurrent workers or Farce-backed application state.
Add dry-types to your application and include both type imports:
require "dry-types"
require "farce"
module Types
include Dry.Types()
include Farce.DryTypes()
end
Farce.DryTypes() follows the preceding Dry.Types() import. Root Farce types
use the corresponding dry default types. Namespaces and aliases gain matching
Farce constants without replacing existing dry types. Loading Farce by itself
does not load dry-types.
The imported constants construct new Farce objects. Use an instance type to check an existing Farce object without conversion:
module Types
VectorInstance = Instance(Farce::Vector)
end
Types::VectorInstance.try(Farce::Vector.new).success? # => true
Types::VectorInstance.try([]).failure? # => true
Vectors
Use Vector.of to apply a member type before constructing the vector:
module Types
IntegerVector = Vector.of(Coercible::Integer)
end
numbers = Types::IntegerVector[["1", 2]]
numbers.class # => Farce::Vector
numbers.to_a # => [1, 2]
numbers.mode # => :copy
The bare Vector accepts any Array members. Imported dry namespaces control
the outer native input in the same way as their Array type:
Types::Vector.try("one").failure? # => true
Types::Coercible::Vector["one"].to_a # => ["one"]
The resulting values compose with optional types, constraints, try, and
failure blocks:
Types::IntegerVector.optional[nil] # => nil
result = Types::IntegerVector.try(["invalid"])
result.failure? # => true
Types::IntegerVector.(["invalid"]) { |partial| [:invalid, partial] }
# => [:invalid, ["invalid"]]
Member and source coercion complete before Farce construction. Their failure blocks receive dry-types' partial native value, never a partially initialized Farce collection. Constraints added to a collection type run on its constructed Farce result. A size constraint on a Set therefore observes deduplication.
dry-types callable defaults return the block result directly. Construct the typed value inside the block when each use needs a fresh Farce collection:
module Types
EmptyIntegerVector = IntegerVector.default { IntegerVector[[]] }
end
Types::EmptyIntegerVector[].class # => Farce::Vector
Types::EmptyIntegerVector[].empty? # => true
Maps and schemas
Use Map.map for homogeneous key and value types:
module Types
ScoreMap = Map.map(String, Coercible::Integer)
end
scores = Types::ScoreMap["Ada" => "10", "Grace" => 12]
scores.class # => Farce::Map
scores.to_h # => {"Ada" => 10, "Grace" => 12}
dry-types rejects keys that collide after coercion. The integration also rejects identity Hash input with structurally equal keys because a structural Farce Map could otherwise drop an entry.
Use Map.schema for fixed keys, defaults, key transforms, and nested types:
module Types
Batch = Map.schema(
name: String,
ids: IntegerVector,
).strict
end
batch = Types::Batch[name: "nightly", ids: ["10", 20]]
batch[:name] # => "nightly"
batch[:ids].class # => Farce::Vector
batch[:ids].to_a # => [10, 20]
Schema chaining and with_key_transform or with_type_transform retain Farce
construction. A non-strict schema omits unknown keys. Call .strict when
unknown keys should fail.
Sets
Set.of applies its member type before membership removes duplicates:
module Types
IntegerSet = Set.of(Coercible::Integer)
end
values = Types::IntegerSet[["1", 1, "2"]]
values.class # => Farce::Set
values.to_a.sort # => [1, 2]
It accepts Arrays and Ruby Sets. The bare Set accepts any Array members.
Counters and flags
Counter uses the imported dry Integer type. Flag uses the imported dry
Bool type:
counter = Types::Coercible::Counter["3"]
counter.class # => Farce::Counter
counter.value # => 3
flag = Types::Params::Flag["yes"]
flag.class # => Farce::Flag
flag.value # => true
A namespace only gains a Farce type when it has the corresponding dry type.
For example, dry-types defines Coercible::Integer but no Coercible::Bool,
so Types::Coercible::Counter exists while Types::Coercible::Flag does not.
The dry type validates the initial scalar. Counter and Flag retain their normal Farce APIs after construction.
Atoms
The bare Atom accepts any initial value. Use Atom.of to validate or coerce
the initial contents:
module Types
IntegerAtom = Atom.of(Coercible::Integer)
end
atom = Types::IntegerAtom["4"]
atom.class # => Farce::Atom
atom.value # => 4
The type applies only at construction. Later writes use the normal Atom API and are not revalidated:
atom.value = "later"
atom.value # => "later"
Nil as Atom contents differs from an optional Atom constructor:
Types::Atom[nil].value # => nil
Types::Atom.of(Types::Integer.optional)[nil].value # => nil
Types::Atom.of(Types::Integer).optional[nil] # => nil
The first two expressions construct an Atom containing nil. The last expression returns nil without constructing an Atom.
Dry imports and Farce variants
With no dry namespace arguments, Farce.DryTypes() inherits the closest
Dry.Types() import. With no preceding import, it uses the same strict defaults
as Dry.Types(). Pass dry namespace arguments, default:, and aliases to
select an independent source using the normal Dry.Types() rules:
module CoercingTypes
include Dry.Types()
include Farce.DryTypes(:strict, :coercible, default: :coercible)
end
CoercingTypes::Vector["1"].to_a # => ["1"]
The dry namespace controls validation and coercion of native input. variant:
selects the Farce class produced after that succeeds:
module LocalTypes
include Dry.Types(default: :coercible)
include Farce.DryTypes(variant: :local, scope: :fiber)
end
vector = LocalTypes::Vector["job"]
vector.class # => Farce::Local::Vector
vector.scope # => :fiber
Supported variants are :shared, :strict, :unshared, and :local.
:shared is the default and accepts mode:. :local accepts scope:.
Strict and Unshared variants accept neither option. Farce's :strict variant
and dry-types' Strict namespace configure separate parts of the conversion.
variant: |
Constructed classes |
|---|---|
:shared |
Farce::Vector, Farce::Map, Farce::Set, Farce::Counter, Farce::Flag, Farce::Atom |
:strict |
Farce::Strict::Vector, Farce::Strict::Map, Farce::Strict::Set, Farce::Strict::Atom |
:unshared |
Farce::Unshared::Vector, Farce::Unshared::Map, Farce::Unshared::Set |
:local |
Farce::Local::Vector, Farce::Local::Map, Farce::Local::Set, Farce::Local::Counter, Farce::Local::Flag, Farce::Local::Atom |
The integration imports only classes that Farce provides for the selected variant. Strict has an Atom but no Counter or Flag. Unshared has none of these three scalar-backed types.
For a single shared collection type, use .with(mode: ...):
module Types
LocalValueVector = Vector.with(mode: :local)
end
payload = []
vector = Types::LocalValueVector[[payload]]
vector[0].equal?(payload) # => true
The supported shared modes are :copy, :local, :make_shareable,
:shareable_copy, and :raise. They apply to collections and Atom. Counter and
Flag have no transfer mode. :move is rejected because dry-types creates and
examines intermediate values during coercion.
Farce input and ownership
Mode-free Strict, Unshared, and Local Farce collections can be converted directly. Construction always returns a fresh configured variant.
Mode-backed Farce::Vector, Farce::Map, and Farce::Set input is rejected
before traversal. Materialize one explicitly when reading it is intended:
source = Farce::Vector.new(["1"])
converted = Types::IntegerVector[source.to_a]
converted.to_a # => [1]
The explicit read establishes where transfer and ownership happen. This rule
also applies when the container's default mode is :copy because an individual
entry might have been inserted with mode: :move.
Vector materialization uses its normal snapshot. Map and set materialization uses normal iteration and is not a globally atomic snapshot during concurrent mutation. Validation describes the values processed by that call. Later writes through the Farce API are not revalidated. Atom input is treated as its payload and is never implicitly read from an existing Atom. Mutable values keep the guarantees of the selected Farce mode. The dry type descriptor is application configuration and is not promised to be Ractor-shareable.