Class: Plumb::Constraint
- Inherits:
-
Object
- Object
- Plumb::Constraint
- Includes:
- Composable
- Defined in:
- lib/plumb/constraint.rb
Constant Summary collapse
- EMPTY =
Sentinel returned by
merge_matchers/intersect_rangeswhen two same-kind matchers have a PROVABLY-EMPTY overlap (disjoint Ranges, empty Set intersection). Distinct fromnil, which means "not the same knowable kind, or an incomputable overlap — leave the two matchers stacked". Callers map EMPTY toTypes::Never, soInteger[0..5][10..]==Integer[0..5] & Integer[10..]Types::Never(see .narrow and Subtyping.intersect_constraints). ::Object.new
- LITERAL_MATCHERS =
Matcher kinds whose
#===is just#==— they match exactly ONE value. Two distinct ones are therefore provably disjoint, and as a#>>consumer one accepts only its own value. Deliberately excludes Module/Range/Regexp/ Set/proc: their#===is a type, membership or pattern test that matches many values (andSet#===is#include?on Ruby >= 3.1). [::String, ::Symbol, ::Numeric, ::TrueClass, ::FalseClass, ::NilClass].freeze
Instance Attribute Summary collapse
-
#base ⇒ Object
readonly
Returns the value of attribute base.
-
#children ⇒ Object
readonly
Returns the value of attribute children.
-
#matcher ⇒ Object
readonly
Returns the value of attribute matcher.
Class Method Summary collapse
-
.literal_matcher?(matcher) ⇒ Boolean
The matcher-level form of #literal? for normalization callers that do not yet have a Constraint.
-
.merge_matchers(a, b) ⇒ Object
Intersection of two knowable matchers of the same kind.
-
.narrow(base, matcher) ⇒ Object
Smart refinement constructor: builds
Constraint.new(matcher, base:), but when bothbase(a Constraint) andmatcherare the same kind of knowable matcher (Ranges or Sets), it INTERSECTS them into one overbase's own base rather than stacking two checks — soInteger[0..100][10..]isInteger[10..100],Integer[Set[1,2,3]][Set[2,3,4]]isInteger[Set[2,3]], andInteger[0..100] >> Integer[0..]reduces toInteger[0..100].
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Compares matchers and bases.
-
#accepted_type ⇒ Composable
A converting base runs before the matcher, so this constraint consumes what the base accepts.
- #call(result) ⇒ Object
-
#idempotent? ⇒ Boolean
Whether the base and matcher are safe to deduplicate.
-
#initialize(matcher = Undefined, base: nil, error: nil, label: nil) ⇒ Constraint
constructor
A new instance of Constraint.
-
#input_type ⇒ Object
As the consumer of a
left >> selfchain: - a refinement (has a base), a Class/Module gate, or a literal matcher accepts exactly what it validates — itself — soInteger >> Integer[1..10]andAny[5] >> Any[6]are correctly rejected (narrow with#[]instead), and the default#accepted_type(its resolved input) isself; - a bare pattern matcher (regex/range/set/proc with no base) narrows arbitrary input over a domain it cannot name, so it reports Any and opts out of the check. -
#literal? ⇒ Boolean
Whether this constraint matches a SINGLE value, by equality — so two distinct ones are provably disjoint (see Subtyping#literal_value).
-
#subtype_identity ⇒ Composable
Projects a matcher over a conversion onto the narrowed output type.
-
#subtype_of?(other) ⇒ Boolean
Structural subtyping.
-
#value_preserving? ⇒ Boolean
A constraint may wrap a converting base; treating it as a pure filter would let reductions discard that conversion.
Methods included from Composable
#&, #/, #>>, #[], #absorb_input, #absorb_output, #as_node, #build, #check, #defer, #fusable_step?, #fuse_with, #generate, included, #invalid, #invoke, #match, #metadata, #not, #output_type, #pipeline, #policy, resolve_operand, #static, #to_json_schema, #to_mermaid, #to_plumb_type, #to_s, #transform, #value, #where, #with, wrap, #|
Methods included from Callable
Constructor Details
#initialize(matcher = Undefined, base: nil, error: nil, label: nil) ⇒ Constraint
Returns a new instance of Constraint.
42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/plumb/constraint.rb', line 42 def initialize(matcher = Undefined, base: nil, error: nil, label: nil) raise ParseError, 'matcher must respond to #===' unless matcher.respond_to?(:===) raise_if_incompatible!(base, matcher) if base @matcher = matcher @matcher_node = SemanticMatcher.wrap(matcher) @base = base @error = error.nil? ? build_error(matcher) : (error % matcher) @label = matcher.is_a?(Class) ? matcher.inspect : "Constraint(#{label || @matcher.inspect})" @children = [matcher].freeze freeze end |
Instance Attribute Details
#base ⇒ Object (readonly)
Returns the value of attribute base.
33 34 35 |
# File 'lib/plumb/constraint.rb', line 33 def base @base end |
#children ⇒ Object (readonly)
Returns the value of attribute children.
33 34 35 |
# File 'lib/plumb/constraint.rb', line 33 def children @children end |
#matcher ⇒ Object (readonly)
Returns the value of attribute matcher.
33 34 35 |
# File 'lib/plumb/constraint.rb', line 33 def matcher @matcher end |
Class Method Details
.literal_matcher?(matcher) ⇒ Boolean
The matcher-level form of #literal? for normalization callers that do not yet have a Constraint. Prefer #literal? on an existing Constraint.
31 |
# File 'lib/plumb/constraint.rb', line 31 def self.literal_matcher?(matcher) = SemanticMatcher.singleton?(matcher) |
.merge_matchers(a, b) ⇒ Object
Intersection of two knowable matchers of the same kind. Returns the merged
matcher on a non-empty overlap, EMPTY when the overlap is provably empty
(disjoint Ranges / empty Set intersection — callers reduce it to Never), or
nil to not merge (different kinds, or an incomputable Range overlap — leave
the two matchers stacked). Public because AttributeValueMatch narrowing
reuses it (see Subtyping) — an attribute constraint intersects its Range/Set
values just like a Constraint.
79 80 81 82 83 84 85 |
# File 'lib/plumb/constraint.rb', line 79 def self.merge_matchers(a, b) merged = SemanticMatcher.merge(a, b) return EMPTY if merged.empty? return merged.node.raw if merged.merged? nil end |
.narrow(base, matcher) ⇒ Object
Smart refinement constructor: builds Constraint.new(matcher, base:), but
when both base (a Constraint) and matcher are the same kind of knowable
matcher (Ranges or Sets), it INTERSECTS them into one over base's own base
rather than stacking two checks — so Integer[0..100][10..] is
Integer[10..100], Integer[Set[1,2,3]][Set[2,3,4]] is Integer[Set[2,3]],
and Integer[0..100] >> Integer[0..] reduces to Integer[0..100]. Other
matchers stack as before.
63 64 65 66 67 68 69 70 |
# File 'lib/plumb/constraint.rb', line 63 def self.narrow(base, matcher) if base.is_a?(Constraint) merged = merge_matchers(base.matcher, matcher) return Types::Never if merged.equal?(EMPTY) # provably-empty overlap ⇒ bottom return narrow(base.base, merged) if merged # recurse: base.base may be a type gate end new(matcher, base:) end |
Instance Method Details
#==(other) ⇒ Boolean
Compares matchers and bases. Fresh Procs lack structural equality, so Proc matchers use their caller-supplied label as stable identity.
175 176 177 178 179 180 181 182 183 |
# File 'lib/plumb/constraint.rb', line 175 def ==(other) return false unless other.instance_of?(self.class) && other.base == base if matcher.is_a?(::Proc) other.matcher.is_a?(::Proc) && other.label == label else other.matcher == matcher end end |
#accepted_type ⇒ Composable
A converting base runs before the matcher, so this constraint consumes what the base accepts. Pure refinements accept themselves.
112 113 114 115 116 |
# File 'lib/plumb/constraint.rb', line 112 def accepted_type return self unless @base && !Plumb::Subtyping.value_preserving?(@base) Plumb::Subtyping.accepted_type(@base) end |
#call(result) ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/plumb/constraint.rb', line 185 def call(result) # Only the base can invalidate the incoming result (callers — map/Or/resolve # — always pass a valid one), so the `valid?` short-circuit is only needed # when there IS a base. Root constraints skip straight to the matcher check, # like a flat leaf. Reading @base directly (not the #base reader) also skips # a dispatch on this hot path. if @base result = @base.call(result) return result unless result.valid? end @matcher === result.value ? result : result.invalid!(errors: @error) end |
#idempotent? ⇒ Boolean
Returns whether the base and matcher are safe to deduplicate.
107 |
# File 'lib/plumb/constraint.rb', line 107 def idempotent? = @base.nil? || @base.idempotent? |
#input_type ⇒ Object
As the consumer of a left >> self chain:
- a refinement (has a base), a Class/Module gate, or a literal matcher
accepts exactly what it validates — itself — so `Integer >>
Integer[1..10]` and `Any[5] >> Any[6]` are correctly rejected (narrow
with `#[]` instead), and the default `#accepted_type` (its resolved
input) is `self`;
- a bare pattern matcher (regex/range/set/proc with no base) narrows
arbitrary input over a domain it cannot name, so it reports Any and opts
out of the check.
96 97 98 99 100 |
# File 'lib/plumb/constraint.rb', line 96 def input_type return self if base || @matcher_node.nominal? || literal? Types::Any end |
#literal? ⇒ Boolean
Whether this constraint matches a SINGLE value, by equality — so two distinct ones are provably disjoint (see Subtyping#literal_value).
104 |
# File 'lib/plumb/constraint.rb', line 104 def literal? = @matcher_node.singleton? |
#subtype_identity ⇒ Composable
Projects a matcher over a conversion onto the narrowed output type. Without this, subtype checks would treat the node as consuming the value it produces.
121 122 123 124 125 126 |
# File 'lib/plumb/constraint.rb', line 121 def subtype_identity return self if @base.nil? || Plumb::Subtyping.value_preserving?(@base) produced = Plumb::Subtyping.resolved_output(@base) produced.equal?(@base) ? self : Constraint.narrow(produced, @matcher) end |
#subtype_of?(other) ⇒ Boolean
Structural subtyping. A matcher describes the set base ∩ {x | matcher === x}
(base = everything when nil). self <= other when self's set is contained in
each of other's conjuncts: within other's base (if any) AND within other's
matcher. Against a non-matcher type, a refinement is a subtype of whatever
its base is (like AttributeValueMatch).
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/plumb/constraint.rb', line 138 def subtype_of?(other) return true if self == other if other.is_a?(Constraint) within_base = other.base.nil? || Plumb::Subtyping.subtype?(self, other.base) return true if within_base && within_matcher?(other.matcher) # `self` is `base ∩ {matcher}` — a subset of `base` — so whenever the # base alone is a subtype of `other`, so is `self`. This is the same # fallback the non-Constraint branch below uses; it belongs here too # because base types (`Types::String`, `Types::Date`) ARE Constraints, # and `within_matcher?` can't peel a transparent base (a Metadata/Policy # wrapper, eg. `String.metadata(...).present`) to see the guarantee. base ? Plumb::Subtyping.subtype?(base, other) : false elsif base Plumb::Subtyping.subtype?(base, other) else false end end |
#value_preserving? ⇒ Boolean
A constraint may wrap a converting base; treating it as a pure filter would let reductions discard that conversion.
131 |
# File 'lib/plumb/constraint.rb', line 131 def value_preserving? = @base.nil? || Plumb::Subtyping.value_preserving?(@base) |