Repository navigation
Phase 1: core HTTP domain model — the wire model - #40
Merged
Merged
Conversation
Twenty-two files under gems/dexpace-core/lib/dexpace/, each with its sig/ mirror: the error root and InvalidArgumentError, the Model and Builder construction contract, HeaderSyntax, HeaderName, Headers, Status, Method, Protocol, MediaType, PercentEncoding, Query, URL, RequestOptions, Request and Response. The entry file requires them in dependency order, the smoke suite's namespace snapshot admits them, and the runtime surface manifest is regenerated once, deliberately, from the built tree. Satisfies HTTP-3 through HTTP-35, HTTP-46, HTTP-47 and HTTP-53, with HTTP-1, HTTP-2 and SEAM-29 by construction; HTTP-22 and HTTP-48 to HTTP-50 stay under docs/first-release.md. What the first real signatures and models taught the gates, in the same change because the tree is not green without it: gates:rbs_surface admits Data, ArgumentError and StringScanner (core Ruby, or strscan on the require allowlist); rbs:validate loads the allowlisted stdlib signature sets it never had to before; the surface walker lists a Data.define reader only while the model keeps it public; gates:single_instance names the superclass mismatch a second copy of a Data.define model raises instead of dying on it, with its test adjusted to that message; and Layout/LeadingCommentSpace admits Steep's inline annotation, which strict Steep requires on an empty literal.
…ypes (XCUT-15, HTTP-4)
…e Model#with as self
…ion is validated before it is owned (HTTP-18, XCUT-18)
…le tag, so a stateful-encoding String is accepted rather than crashed on (HTTP-13, HTTP-17)
… with as the SDK's error (HTTP-35)
This was referenced Sep 15, 2026
Wahbeh-Mohammad
added this pull request to stack #43
September 15, 2026 09:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8. First of three phase 1 PRs — the code. Its tests are #41 and the phase record is #42. First phase that ships domain code.
What lands
The frozen, transport-agnostic HTTP wire model in
dexpace-core— 53 files, +2,808:Dexpace::Error(a module),Dexpace::InvalidArgumentError < ::ArgumentError,Dexpace::Model(required!with the one<name> is requiredformSEAM-29fixes,own— every collection duplicated and deep-frozen exactly once at construction viaRactor.make_shareable(copy: true),frozen_string, and a#withthat routes through.buildso it re-validates on Ruby 3.2, whereData#withskips aninitializeoverride), and the sharedDexpace::Buildercontract. Every model isclass X < Data.define(...)withprivate_class_method :newand a validating.build; the residualsend(:new, …)/ duck-type gap is asserted in the tests rather than hidden (design §10 item 10).HeaderSyntax: byte-level predicates (.b.each_byte, never a character regexp) forHTTP-17–HTTP-20, public because phase 8's adapters re-run them at the wire boundary; theString#strip-strips-NUL trap avoided.HeaderNamewithHTTP-13's locale-free fold;Headers+Headers::Builderwith direction (inbound/outbound), multi-value and ordering semantics.Status(total over 100–599),Method(HTTP-7/HTTP-8body legality;IDEMPOTENTpublic per P1-10),Protocol,MediaType(aStringScannerparser over per-pattern-timeout regexps, run only after the byte check),PercentEncoding(design §3.5's strict component encoder, byte-based),Query+Query::Builder(equality by encoding, P1-12),URL(every parse pinned toURI::RFC3986_PARSER; re-parses a URI input from its text and freezes the component Strings it owns, becauseURI#dupshares@host/@pathwith the caller — P1-13),RequestOptions+ builder (HTTP-35),Request+ builder,Response+ builder.sig/mirrorslib/one file per file; YARD 100% (15 modules, 16 classes, 50 constants, 8 attributes, 123 methods); the runtime-surface manifest regenerated deliberately (2 → 212 lines).tools/rbs_surface.rb's stdlib allowlist gainsData,ArgumentError,StringScanner(a false positive onclass X < Data);gates:single_instancereports thesuperclass mismatcha double-loadedData.defineraises;Surface.data_readershonours a privatised reader;rbs:validateloads the allowlisted stdlib signature sets with-r. PlusLayout/LeadingCommentSpace: AllowRBSInlineAnnotation: true, because strict Steep needs#:annotations on empty literals.Layering, and why this PR's CI is red
Each tip of the stack is green under every gate on its own tree with one stated exception: on this branch
test:gemsfails on the SimpleCovminimum_coverage 80floor alone — 60.58%, 20 runs, 0 failures — because the suites that raise coverage are #41's by definition. The other sixteen gates were run individually and are green (rubocop, cops:test, rbs:validate, steep, test:gates, all ninegates:*, yard, bundler_audit). #41 takes the same tree to 100% line coverage (789/789) and is fully green on 4.0.6 and on the 3.2.11 floor. The two gate test files here (test/gates/single_instance_test.rb,gems/dexpace-core/test/dexpace_test.rb) are the ones the code alone breaks.Verification
#withre-validates on the floor; every rejection isDexpace::InvalidArgumentError, never anArgumentError/Encoding::CompatibilityErrorleaking from inside Ruby on invalid UTF-8, CR, LF or NUL;HTTP-7/HTTP-8via builder,.buildand#with;I/İ/ı/ßper the design; noURI.parse/URI.join/DEFAULT_PARSER, noRegexp.timeout=, noTime.parse, no interrupts.Ractor.shareable?as built,Request/Responseincluded (with anilor frozen body) — design §4's claim stands in full; the planning-time narrowing P1-9 is retired (Phase 1: core HTTP domain model — documentation and phase record #42 records it).Known follow-ups from the final review (not blocking)
Model.ownon a Hash with adefault_proc(Hash.new { |h, k| h[k] = [] }, the ordinary multimap idiom): passes the shape checks, thenRactor.make_shareable(copy: true)raisesTypeError: allocator undefined for Proc, which escapesrescue Dexpace::ErrorfromHeaders.build(values:/casing:),RequestOptions.build(tags:)andRequestOptions::Builder#build. Identical on 3.2.11 and 4.0.6. Should be caught and re-raised asInvalidArgumentError(or the proc dropped) inModel.own.HeaderSyntax.valid_name?(nil),token?(nil),validate_outbound_value!(1, …)andHeaders::Builder.new(values: 5)raiseNoMethodErrorrather than the SDK's error — these are exactly the predicates phase 8 re-runs on a possibly forged model, so aString ===guard would keep the Task 1 rule uniform.Query.parseusestext.b.strip, which drops a raw leading/trailing NUL; the header path deliberately trims SP/HTAB only. Either match, or state it in the YARD block.