Skip to content

feat(support): Uri fluent parser with immutable Uriable builder#121

Merged
bedus-creation merged 2 commits into
mainfrom
task/uri-fluent-parser
Jun 13, 2026
Merged

feat(support): Uri fluent parser with immutable Uriable builder#121
bedus-creation merged 2 commits into
mainfrom
task/uri-fluent-parser

Conversation

@bedus-creation

Copy link
Copy Markdown
Contributor

Summary

  • Implements Uri.of(url)Uriable fluent API modelled on the existing Str/Stringable pattern in fastapi_startkit.support
  • Uriable wraps urllib.parse.ParseResult and exposes:
    • Read accessors: scheme(), host(), port(), path(), query(), query_param(), fragment()
    • Immutable mutators: with_scheme(), with_host(), with_port(), with_path(), append_path(), with_query(), add_query(), remove_query(), with_fragment(), without_fragment(), without_query()
    • get() / __str__() to retrieve the final URL string
  • Every mutator returns a new Uriable (value-object semantics — original is never modified)
  • Uri and Uriable are exported from fastapi_startkit.support
  • Auth info (user:pass@) and port are preserved across mutations

Test plan

  • 48 new tests in tests/utils/test_uri.py covering: factory methods, all accessors, each mutator, full fluent chain, immutability across the chain, equality (== to string and another Uriable), and __repr__
  • Full suite (1037 tests) passes with no regressions

Closes #178

🤖 Generated with Claude Code

Implements Uri.of(url) → Uriable fluent API modelled on the existing Str/Stringable
pattern.  Uriable wraps urllib.parse.ParseResult and exposes read accessors
(scheme, host, port, path, query, fragment) alongside immutable mutators
(with_scheme, with_host, with_port, with_path, append_path, with_query,
add_query, remove_query, with_fragment, without_fragment, without_query).
Every mutator returns a new Uriable so the original is never modified.

Exports Uri and Uriable from fastapi_startkit.support.  48 new tests added in
tests/utils/test_uri.py covering accessors, each mutator, fluent chaining,
immutability, equality, and __repr__.

Closes #178

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bedus-creation

Copy link
Copy Markdown
Contributor Author

✅ Code Review: APPROVED (with notes)

48/48 URI tests pass. Full suite: 1450 passed, 7 skipped — zero regressions.


Checklist walkthrough

1. Uri/Uriable structure and immutability ✓

Pattern correctly mirrors Str/Stringable:

  • Uriable = fluent builder, wraps ParseResult + internal dict[str, list[str]] for query state
  • Every mutator calls _rebuild() which instantiates a new Uriable — original is never touched
  • TestFluencyChaining::test_immutability_across_chain confirms this structurally and behaviourally ✓

Auth preservation across mutations also verified manually:

Uri.of('https://user:pass@example.com:8080/api').with_host('other.com').get()
→ 'https://user:pass@other.com:8080/api'  ✓

2. support/init.py exports ✓

Both Uri and Uriable exported and listed in __all__. ✓

3. serve_command.py — urllib.parse → Uri.of() migration ℹ️ (N/A for this PR)

On this branch serve_command.py is the main version (pre-PR #111 rewrite). There is no urllib.parse import in it to replace. The _resolve_host_port + urlparse code lives only in PR #111's branch, which hasn't landed. The migration to Uri.of() belongs in a revised PR #111 — not here.

4. Tests in tests/utils/test_uri.py ✓

All 48 tests present. Fine location.

5. query().all(), .has(), .missing(), .decode() — ⚠️ Not implemented (see note)

6. No regressions ✓ (inferring the truncated checklist item)


Correctness spot-checks

Scenario Result
Round-trip: Uri.of(url).get() == url
without_query() — no trailing ?
without_fragment() — no trailing #
with_query({}) clears all params
Multi-value params via with_query({"ids": [1, 2, 3]})
add_query replaces an existing key
remove_query no-ops on missing key
Auth user:pass@ preserved after with_host / with_port

Notes (non-blocking)

Note 1 — query() fluent methods not implemented (spec deviation)

PM checklist item 5 specified query().all(), .has(), .missing(), .decode(). The SE instead implemented query() → dict[str, list[str]] plus query_param(key, default) for single-key access.

The SE's approach is simpler and idiomatic Python — callers can use standard dict operations ("page" in uri.query(), uri.query().get("page")). However it differs from what was spec'd. Suggest a follow-up task if a fluent QueryString wrapper (.has(), .missing(), .decode()) is still wanted.

Note 2 — Schemaless URLs require a scheme

Uri.of('myapp.com:9000') returns host='', port=None because urlparse treats myapp.com as the scheme. This is standard urlparse behaviour — not a bug. If PR #111's _resolve_host_port is eventually rewritten to use Uri.of(), it must continue to prepend http:// for schemaless URLs before calling Uri.of(). Worth a one-liner note in the Uri.of() docstring.

Note 3 — _build_netloc uses Ellipsis sentinel with # type: ignore

Functional, but a named sentinel (_UNSET = object()) would be cleaner. Minor — not a blocker.


Verdict: APPROVED — safe to merge.

Remove unused `pytest` import and reformat chained method call in tests/utils/test_uri.py to satisfy ruff lint (F401) and ruff format checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bedus-creation bedus-creation merged commit e823595 into main Jun 13, 2026
3 checks passed
@bedus-creation bedus-creation deleted the task/uri-fluent-parser branch June 13, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant