Skip to content

Actionable v1.6.0 — valid, guarded RBS

Choose a tag to compare

@aemadrid-twx aemadrid-twx released this 22 Oct 23:01
· 74 commits to main since this release
35e06aa

Valid, guarded RBS. The gem's own signatures can only get more typed (CI
ratchet, untyped 46 → 27); generated RBS for your actions declares the real
superclass chain, a typed InputSchema, typed .run keywords and output
ivars, built on field_struct 0.12's FieldStruct::RBS.type_for. Requires
field_struct >= 0.12. No runtime behaviour changes.

Added

  • Actionable::RBS.generate types an action's input and output instance
    variables
    (generated RBS only; no runtime change). For an action with a
    fixed input schema it emits a nested class InputSchema < ::FieldStruct::Base
    with a typed reader and writer per input field, def input: () -> InputSchema,
    and a .run overload | (InputSchema input) -> ... beside the keyword form.
    InputSchema exists only in RBS (the runtime schema is anonymous), so
    generation raises ArgumentError when Ruby already resolves
    YourAction::InputSchema (a constant on the action, an ancestor or an
    included module). It also declares each output field as a nullable
    instance variable (@total: ::Integer?), so a step that assigns
    @total = ... type-checks. Upgrade note: the generated RBS now owns
    those instance variables; delete any declaration of them from your own sig
    files, since RBS rejects an instance variable declared twice.

Fixed

  • Actionable::RBS.generate declares the real superclass chain. An action
    under an intermediate base (class Codes::DecideMapping < ApplicationAction)
    was declared < ::Actionable::Action, hiding its bases from the type
    checker, and a host that declared the real superclass got
    NoSuperclassFoundError / a failing rbs validate when the base had no
    signature. The action now names its nearest named superclass, and every
    named ancestor up to Actionable::Action is declared first, member-less
    (a typed one with its empty result classes), so the RBS resolves on its own.
    The declarations repeat byte-identically across files and merge. A namespace
    that is a class now opens as class, not module, which RBS rejected as a
    duplicate declaration.
    Upgrade note: if your own sig files declare one of those bases with a
    different superclass (e.g. class ApplicationAction < ::Actionable::Action
    while Ruby says otherwise), RBS now reports a superclass mismatch; make your
    declaration match Ruby's ancestry or drop it.

Changed

  • Optional .run keywords are nullable in generated RBS (?note: ::String?): an optional field may be passed nil explicitly, and the run succeeds.
  • A required input field with a default is an optional .run keyword (?amount: ::Integer), since run succeeds without it. An action whose input is chosen by an input_for dispatch (including one inherited under a fixed input) gets .run: (*untyped), since the dispatch decides the keywords at run time.
  • Requires field_struct >= 0.12. The generator now takes every field type
    from FieldStruct::RBS.type_for instead of its own copy of the mapping, so
    generated action RBS matches FieldStruct::RBS.generate. Consumer-visible
    changes to generated RBS: writers on Output and the per-code output
    classes are typed (T? value) -> T? instead of (untyped value) -> untyped; scalar array elements are nullable (::Array[::String?]); a
    :value field is untyped, never untyped?; a bare generic ruby_type
    gets type arguments (::Hash[untyped, untyped]); and a field's rbs_type:
    override is honoured. A field typed by an anonymous nested struct class now
    raises ArgumentError (it previously emitted an invalid :: name).
  • The committed signatures can only get more typed. Contributor tooling
    only; the shipped sig/actionable.rbs is unchanged. rake sigs:untyped
    (part of the default task) fails when sig/ holds more or fewer than the
    committed ceiling of 46 non-comment untyped, so a typing change lowers
    the ceiling with it. sigs:generate / sigs:check fail on a sord
    [OMIT]/[DUCK] fallback that isn't in the committed allowlist (24
    today) and on allowlist entries sord no longer emits. sord runs without
    --replace-errors-with-untyped (the output is identical) and fails on any
    SORD_ERROR_ type. sigs:validate validates the whole sig/ directory.
  • Optional, report-only Steep 2.x via gemfiles/types.gemfile and a
    types CI job on Ruby 3.4 that never fails the build. The Steepfile now
    checks the core only (the RSpec and Rails adapters are ignored); Steep is
    no longer in the main Gemfile.
  • More precise signatures (RBS only; no runtime change). Keyword splats
    are typed per value (**Object options, not **::Hash[Symbol, Object] options, which RBS reads as "every value is a Hash"); .run, #initialize
    and Job::Mixin#run_actionable take *Object, **Object; the gem's
    constants (Actionable::HALT, VERSION, Action::GEM_FAILURE_CODES, …)
    are declared; and the halt/measurement helpers declare their block. The
    DSL methods whose block runs with another self (output, input,
    fails, skips, case_step, input_for, and the define builders)
    gain a block overload with its self-type in the new
    sig/actionable/dsl.rbs. untyped in sig/ falls from 46 to 27, and the
    sord fallback allowlist from 24 entries to 4.