Skip to content

feat: link tags - #1

Merged
Zingzy merged 1 commit into
mainfrom
feat/link-tags
Sep 4, 2026
Merged

feat: link tags#1
Zingzy merged 1 commit into
mainfrom
feat/link-tags

Conversation

@Zingzy

@Zingzy Zingzy commented Sep 4, 2026

Copy link
Copy Markdown
Member

What

  • Tag and TagRef types; ListTags, CreateTag, UpdateTag (PATCH, every field optional), DeleteTag (reports links_updated)
  • TagIDs on ShortenRequest (omitted when empty) and UpdateURLParams (Opt[[]string]: Set replaces the list, Set([]string{}) or Null clears it, omitted keeps it)
  • Tags []TagRef on ShortURL, URLItem and UpdatedURL
  • TagIDs, TagNames, TagsMatch on ListURLsOptions, serialised into the existing filter JSON as tagIds, tagNames, tagsMatch
  • tag and tag_id stats and export filters through StatsQuery.Filters, comma-joined like the other filters; rejected client-side on the per-link routes like short_code and url_id
  • BulkTags(ctx, ids, add, remove) posting {ids, add, remove} to /api/v1/urls/bulk/tags, returning *BulkResult
  • Tests for each of the above, a README section, and openapi.json refreshed from upstream main so the spec drift job passes

Why

Tags are live on the API (spoo v2.4.0) and this SDK is how people script against it. Without these methods the only route was the raw request passthroughs.

How proven

gofmt -l .          -> no output
go build ./...      -> ok
go vet ./...        -> ok
go test -race ./... -> ok  github.com/spoo-me/spoo-go
                       ok  github.com/spoo-me/spoo-go/internal/transport
golangci-lint run   -> 0 issues.

The 14 new tests (go test -run 'Tag|BulkTags' -v .) all pass and assert method, path, query and exact body against httptest servers. Not run against production.

Summary by CodeRabbit

  • New Features
    • Added tag management, including creating, listing, updating, and deleting tags.
    • Added tag support when creating, updating, listing, and retrieving links.
    • Added filtering by tag IDs or names, with any/all matching options.
    • Added bulk tag operations for up to 100 links.
    • Added tag-based filtering for statistics and exports.
    • Updated API documentation to cover tag functionality.

Copilot AI lite review requested due to automatic review settings September 4, 2026 00:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 47 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 160d1a13-c85b-43f7-84bb-b4621ed6599a

📥 Commits

Reviewing files that changed from the base of the PR and between caba6aa and 21d0c11.

📒 Files selected for processing (9)
  • README.md
  • bulk.go
  • bulk_test.go
  • export.go
  • stats.go
  • stats_test.go
  • tags.go
  • tags_test.go
  • urls.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 814de1a8-60e8-4038-8246-8d0c8abbbca9

📥 Commits

Reviewing files that changed from the base of the PR and between 646a22a and caba6aa.

📒 Files selected for processing (14)
  • README.md
  • bulk.go
  • bulk_test.go
  • doc.go
  • export.go
  • openapi.json
  • shorten.go
  • shorten_test.go
  • stats.go
  • stats_test.go
  • tags.go
  • tags_test.go
  • urls.go
  • urls_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The SDK adds tag CRUD operations, link tagging and filtering, bulk tag updates, tag filters for statistics and exports, and documentation and tests for these capabilities.

Changes

Tag support

Layer / File(s) Summary
Tag resource API
tags.go, tags_test.go
Adds typed tag resources and CRUD methods for listing, creating, updating, and deleting tags. Tests cover request bodies, timestamps, deletion results, and not-found errors.
Link tagging and filtering
shorten.go, shorten_test.go, urls.go, urls_test.go
Adds tag IDs to link creation and updates, tag references to responses, tag-based URL filters, and tag replacement or clearing semantics.
Bulk tag updates
bulk.go, bulk_test.go
Adds BulkTags for adding and removing tags from links with per-item results.
Statistics, exports, and documentation
stats.go, stats_test.go, export.go, README.md, doc.go
Adds aggregate tag and tag_id filters, rejects them for per-link calls, and documents the tag APIs and endpoints.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to caba6

This change adds SDK tag management, link tagging, filtering, bulk updates, and tag-aware stats and exports. The covered request and response behaviors are ready to merge with no identified current risk.

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant spoo.Client
  participant c.do
  participant TagsAPI
  Application->>spoo.Client: Call CreateTag, UpdateTag, or DeleteTag
  spoo.Client->>c.do: Build and send typed request
  c.do->>TagsAPI: Issue HTTP request to /api/v1/tags
  TagsAPI-->>c.do: Return tag response or error
  c.do-->>spoo.Client: Decode typed result
  spoo.Client-->>Application: Return result or error
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 12 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding link tag support across the SDK.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 12 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/link-tags

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Zingzy Zingzy left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Mergeable. Nothing blocks. The wire contract matches the live v2.4.0 spec on every path, method, query key and body field I traced, and the tri-state handling on UpdateURLParams.TagIDs does exactly what the API needs (list replaces, [] or null clears, omitted keeps). One design call to make on the BulkTags signature before it is frozen into a public API; the rest is small.

Checked against the real thing, not the PR body: diffed origin/main...origin/feat/link-tags, read every Go file in full, ran go vet ./... and go test -race -count=1 ./... at caba6aa (both green), and compared the committed openapi.json to upstream spoo/main (byte-for-byte equal as parsed JSON, so the 2,260-line refresh is exactly what the drift job requires and nothing else).

Contract trace

Every item below was verified by reading the SDK code and its test assertions against the spec, not the description.

  • POST /api/v1/shorten: tag_ids omitted when empty (shorten.go:23, asserted in TestShortenTagIDs). Correct, since on create an absent list and an empty list mean the same thing.
  • PATCH /api/v1/urls/{id}: Opt[[]string] with omitzero (urls.go:256). TestUpdateURLTagIDs pins all four bodies: {"tag_ids":["t1","t2"]}, {"tag_ids":[]}, {"tag_ids":null}, and omitted. Matches the spec text "the list replaces the stored one; null or [] clears it".
  • GET /api/v1/urls filter JSON: tagIds, tagNames as arrays, tagsMatch as a string, riding the existing filter param (urls.go:118-126). Matches the filter param description in the spec.
  • GET /api/v1/stats and GET /api/v1/export: tag and tag_id go through the existing values() comma-join, which is the format both query params document. Per-link routes reject them before any request goes out (stats.go:117-124), and TestPerLinkCallsRejectTagFilters proves no request is sent.
  • POST /api/v1/urls/bulk/tags: {ids, add, remove} with empty add/remove omitted (bulk.go:88-92). The spec marks only ids required, so omitting is right.
  • PATCH /api/v1/tags/{id}: UpdateTagParams uses plain strings with omitempty, so the SDK can never emit icon: null (a 422 upstream). Good call, and consistent with how UpdateURLParams uses plain strings for LongURL and Alias and reserves Opt for fields where null means something.
  • Decoding: Tag.Color, Tag.Icon, TagRef.Color, TagRef.Icon are plain strings, so a palette or icon key added server-side later decodes without a release. updated_at: null decodes to a zero Timestamp (TestListTagsUnwrapsItems).
  • Response types: tags is present on UrlResponse, UrlListItem and UpdateUrlResponse in the spec and on ShortURL, URLItem and UpdatedURL in the SDK. ClaimUrlsResponse and the public stats envelope carry no tags upstream, so nothing is missing.

Blocker

None.

Should-decide

1. BulkTags(ctx, ids, add, remove []string) has two adjacent same-typed slice parameters (bulk.go:87). c.BulkTags(ctx, ids, x, nil) and c.BulkTags(ctx, ids, nil, x) both compile and both look right at the call site; the second one strips tags the caller meant to add. The sibling bulk methods dodge this because their second argument is a string or a time.Time, so a swap fails to compile. This is the only bulk method that mutates in two opposite directions from one call, and it is the one where a swap is silent. Blast radius is the caller's own links (recoverable, 200 OK, no error), so should-decide rather than blocker, but a signature is the one thing an SDK cannot fix after v1 users pick it up. Fix: BulkTags(ctx, ids, BulkTagChange{Add: ..., Remove: ...}) with a two-field struct, one request as today, named at the call site. README and the test adjust in place.

Nits

2. Doc claims a 422 the spec does not state (stats.go:117, stats.go:142-144, export.go:43-44). The per-link stats route description says only that short_code and url_id "do not exist here"; it says nothing about tag or tag_id, and its filters dimension list simply omits them. Rejecting client-side is the right behaviour either way (a silently ignored filter would return unfiltered data as if it were filtered), but the comments assert a server status code for tag/tag_id that the contract does not promise. Say "not accepted on the per-link routes" and drop the 422 for the tag pair.

3. The Tag doc comment hard-codes the palette (tags.go:10-12). The type keeps Color and Icon as strings precisely so a new key decodes without an SDK release; the doc enumerating nine colours will be stale the first time that happens and nobody will remember to bump it. Name one or two as examples and point at the API docs for the current set.

4. Spelling register (README.md:123, tags.go:33, three "colour" in prose). The repo writes American everywhere (serialize in opt.go and timestamp.go, behavior in version_test.go) and the field is Color. The API spec descriptions use British, which is probably where it came from, but the SDK prose should match itself.

5. Set([]string(nil)) marshals as null (urls.go:244-246). Behaviourally identical to Null[[]string]() since both clear upstream, so no bug. One clause in the UpdateURLParams doc ("a nil slice clears like Null") saves someone a test.

Public text

Clean. Grepped every added line, the commit message and the PR body for em-dashes, en-dashes, AI or agent mentions, and ticket ids: none. The CodeRabbit block in the PR body is bot-appended, not author text.

Comments

Fit the house style. // zero until the tag is first edited on Tag.UpdatedAt states a nullable the type cannot show, and the one-line headers on the new tests match the ones already in shorten_test.go and bulk_test.go. Nothing narrates a change or talks to the reviewer.

Merge and release

Merging publishes nothing; a Go module version tag does. The API side is live for every account with no flag, so once a tag is cut this is usable immediately. The Version = "dev" telemetry string is a pre-existing issue and not this PR's to fix, but it will be visible on the first request users make with the new methods.

Good

  • Every new test asserts the exact request body or query as a string, not just "a request happened". TestUpdateURLTagIDs pinning all four bodies is the kind of test that catches a future omitempty/omitzero mixup.
  • UpdateTagParams not reaching for Opt was the right instinct; Opt stays where null carries meaning, exactly as its doc comment promises.
  • TagDeletion next to DomainDeletion, ListTags unwrapping items the same way the other list calls do, path ids through url.PathEscape like UpdateURL. Nothing here reads as if a different person wrote it.

Tags went live on the API in spoo v2.4.0 and this SDK had no way to
reach them. Adds the tag endpoints, tag_ids on shorten and update, tags
on every link response, the tagIds/tagNames/tagsMatch list filters, the
tag and tag_id stats and export filters, and BulkUpdateTags next to the
other bulk methods. Refreshes openapi.json so spec drift stays green.
@Zingzy

Zingzy commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Applied all five findings in the same commit.

  1. BulkTags(ctx, ids, add, remove) is now BulkUpdateTags(ctx, ids, BulkTagChange{Add, Remove}), so the two directions are named at the call site and a swap no longer compiles. Named to match BulkUpdateStatus and BulkUpdateExpiry here and the bulk update tags methods in the other SDKs. README, test and coverage table updated.
  2. The per-link stats and export doc comments now say the slicing filters are not accepted on the per-link routes and are rejected client-side; the 422 claim for the tag pair is gone.
  3. The Tag doc comment gives "violet", "teal" and "rocket" as examples and points at the API docs for the current sets instead of listing the palette.
  4. Prose is American throughout: colour became color in tags.go, tags_test.go and the README.
  5. UpdateURLParams now says a nil slice passed to Set clears the tags like Null.

gofmt, go vet, go test -race and golangci-lint are all green at the new head.

@Zingzy
Zingzy merged commit 73f7ba3 into main Sep 4, 2026
6 checks passed
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.

2 participants