Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 17 additions & 1 deletion .scalafix.conf
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,27 @@ DisableSyntax {
}

// Order per SCALA_CODE_STYLE.md § Imports: worxbend, ecosystem, third-party, scala, java.
//
// `groupedImports = Merge` — several names from one package are collected into a
// single braced import rather than exploded onto one line each.
//
// MEASURED, NOT PREFERRED. Under `Explode`, promoting the shared codec helpers
// gave 108 files in `codec` the same seven-line preamble, and PMD CPD — which
// has no import filter for Scala — counted every pair of them as duplication.
// 324 of 408 reported groups contained no code beyond the file preamble, so the
// duplication gate in verify.sh was mostly measuring imports. `Merge` collapses
// each preamble to one line per package and the count falls below the recorded
// baseline, which is what makes the gate mean something again.
//
// This is still the explicit-import style SCALA_CODE_STYLE.md § Imports asks
// for: every name is written out, no wildcards are introduced, and
// `removeUnused` still prunes names inside the braces, so an unused helper
// stays visible.
OrganizeImports {
targetDialect = Scala3
removeUnused = true
expandRelative = true
groupedImports = Explode
groupedImports = Merge
importsOrder = Ascii
blankLines = Auto
groups = [
Expand Down
16 changes: 15 additions & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,21 @@ literals.float = Lower
literals.double = Lower
literals.scientific = Lower

trailingCommas = multiple # required in multiline lists
# `keep`, not `multiple`, because two tools have to agree here.
#
# Scalafix's OrganizeImports emits a merged import with no trailing comma.
# Under `trailingCommas = multiple`, scalafmt added one back to every import
# list long enough to wrap, and `mill __.fix --check` then removed it again —
# the two steps of the verification gate rewriting each other's output, which
# fails the gate no matter which order they run in.
#
# `keep` preserves whatever is written rather than adding or removing, so the
# two converge. It costs the automatic enforcement the previous setting gave:
# a new multiline list written without a trailing comma is no longer corrected
# for you. The style is still what SCALA_CODE_STYLE.md asks for, and every
# multiline list in the tree already carries its comma — this changes no
# existing code, only what is added on your behalf in future.
trailingCommas = keep

# ---------------------------------------------------------------------------
# Project scope
Expand Down
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,59 @@ changed deliberately *before* the freeze, and anyone who built against a
statement and 100.00 % branch coverage, `core` 96.59 % / 92.48 %, `codec`
95.20 % / 91.47 %, and the CRAP gate reports a worst method of 28.0 over
2,217 methods against a limit of 30.
- **Compile-time constructors for the path identifiers.** Each identifier that
is validated as a URI path segment — `Owner`, `RepoName`, `BranchName`,
`TagName`, `Username`, `OrgName` and the rest — now takes a string literal
directly. `Owner("forgejo")` is checked while the code compiles and *is* the
`Owner`, with no `Either` to unwrap, and an invalid literal is a compile
error naming the field: `not a valid owner: "forgejo/forgejo"`. `from` is
unchanged and remains the way in for a value known only at run time; handing
one to the literal constructor is itself a compile error. The check is
`inline` and folds away, so it reaches no bytecode. One deliberate
difference between the two: the literal form refuses surrounding whitespace
where `from` trims it.
- **A single import for the everyday surface.** `Auth`, `Page`, `PageParams`
and `PageSize` are re-exported from the package root, so
`import com.worxbend.codeberg4s.*` covers building an `Auth`, constructing a
client, calling an operation and paging through a listing. The quick start
needed six import lines before.

### Changed

Every item here is a breaking change against the `0.1.0-SNAPSHOT` builds, taken
now because the tag is what freezes the surface.

- **Command and query types can no longer be built without validation.**
Thirteen types paired a validating `of` with a public case-class
constructor, so `apply` and `copy` bypassed the check the Scaladoc
promised — `AddTrackedTime(1500.millis, …)` was accepted and then sent as
one second, because the codec truncates on the stated assumption that the
domain type refuses sub-second durations. Their constructors are now
`private[codeberg4s]`, matching the 131 response models. Build them with
`of` and adjust them with the `with…` builders. The types:
`AddTrackedTime`, `CreateComment`, `CreateIssue`, `CreateMilestone`,
`EditComment`, `CreatePullRequest`, `BranchProtectionSettings`,
`CreateDeployKey`, `DeployKeyQuery`, `CreateWikiPage`, `EditWikiPage`,
`ActivityFeedQuery`, `TrackedTimeWindow`.
- **`organizations.BlockedUser` and `organizations.BlockId` are gone.** The
organisation and account block lists return the same two-property Forgejo
model, and it was declared twice. Import
`com.worxbend.codeberg4s.users.social.BlockedUser` and `BlockId`; the type
is identical. `OrganizationApi.blockedUsers` is unchanged.
- **`Owner` and `RepoName` moved to the package root.** They were in
`com.worxbend.codeberg4s.repositories`, which meant the two types needed
before *any* request could be made lived in a sub-package a caller had no
other reason to know about. Import them from `com.worxbend.codeberg4s`, or
use the single wildcard import above.
- **Every `PageParams` parameter is named `params`, not `pageParams`.** The old
name restated the type instead of saying anything. Only call sites that pass
the argument by name need an edit.
- **`RepositoryApi`'s sub-resource listings are bare nouns.** `listBranches`,
`listTags`, `listCommits`, `listReleases`, `listTopics` and `listForks`
became `branches`, `tags`, `commits`, `releases`, `topics` and `forks`: the
`list` prefix repeated what `client.repos.` had already established. The
`list…` names on `IssueApi`, `RepositoryAccessApi` and `PullRequestApi` are
unchanged, because there the prefix still distinguishes the operation.
- **Response models cannot be constructed from outside the library.** All 131
response types — `Repository`, `Issue`, `PullRequest`, `User`,
`Organization`, `NotificationThread`, `ServerVersion`, `ApiErrorBody` and the
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ repeat is a no-op.

**7. Scaladoc it, with the error contract.** Every public member. Say which
`CodebergError` cases the operation can produce and what a `404` means for this
operation specifically — the ADT has only five cases and none of them is
operation specifically — the ADT has only six cases and none of them is
`NotFound`, so "the repository does not exist" has to be spelled out as
`Api(ctx, 404, body)`.

Expand Down
6 changes: 3 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ Two things this exemption does **not** cover:
the whole cost is one filter line for that model. Those filters are lines to
write, not releases to renumber, because no external caller can have
compiled against the member being filtered.
- **Adding a case to a closed `enum`.** `CodebergError` has exactly five
- **Adding a case to a closed `enum`.** `CodebergError` has exactly six
cases — `Transport`, `Api`, `DecodingFailed`, `Validation`,
`RetriesExhausted` — and every consumer that matches on it exhaustively
stops compiling when a sixth appears. There is deliberately no `NotFound`
`RetriesExhausted`, `WalkTruncated` — and every consumer that matches on it
exhaustively stops compiling when a seventh appears. There is deliberately no `NotFound`
and no `RateLimited`; a `404` is `Api(ctx, 404, body)` and a `429` is
`Api(ctx, 429, body)` or a `RetriesExhausted` wrapping one. Keeping it that
way is a compatibility decision, not only a modelling one.
Expand Down
10 changes: 8 additions & 2 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,16 @@ object modules extends Module:
)
object test extends Codeberg4sTests

/** The only module importing sttp. Implements core's `HttpPort`. */
/** The only module importing sttp. Implements core's `HttpPort`.
*
* It does not depend on `codec`, and that absence is the boundary ADR-0003 describes: the transport reads every
* response body as a `String` and hands it on, so the JSON library stays out of it. Leaving the edge in place made
* that a promise kept only by a grep in `verify.sh` step 6, which an import-free fully-qualified call would walk
* straight past — and it put jsoniter on the published POM of an artifact that never calls it.
*/
object transport extends Codeberg4sPublishModule:
def artifactName = "codeberg4s-transport"
def moduleDeps = Seq(core, codec)
def moduleDeps = Seq(core)

def mvnDeps = Seq(
mvn"com.softwaremill.sttp.client4::core:${Versions.sttp}",
Expand Down
16 changes: 16 additions & 0 deletions docs/LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ done, in the same commit that introduces the model.
| `Organization` | `com.worxbend.codeberg4s.organizations` | wave 5 | — |
| `Team` | `com.worxbend.codeberg4s.organizations` | wave 5 | — |
| `NotificationThread` | `com.worxbend.codeberg4s.notifications` | wave 6 | — |
| `BlockedUser`, `BlockId` | `com.worxbend.codeberg4s.users.social` | wave 1 | organizations (see the collapse below) |

`BlockedUser` was a duplicate, not a prediction. `GET /user/list_blocked` and
`GET /orgs/{org}/list_blocked` return the same two-property model, and each
group had modelled it separately: two `BlockedUser` case classes, two
`opaque type BlockId = Long` with byte-identical `PositiveId.from("blockId", …)`
bodies, two DTOs and two test suites. That is exactly what the rule at the top
of this file calls a review-blocking defect, and it survived because nothing
checked for it — the duplication gate that was supposed to catch it was, at the
time, mostly counting import blocks.

`users.social` keeps them, per the first-wave rule; `organizations` imports
them. The three test cases the organizations suite had and the social one
lacked — the Go zero-time sentinel, the "at least 1" message on a non-positive
id, and the empty listing — were moved into `SocialDtoSuite` rather than
dropped with the file.

Two rows of that table were written before the waves ran and predicted a reuse
that did not happen. Both are corrected rather than deleted, because the
Expand Down
15 changes: 4 additions & 11 deletions modules/client/src/com/worxbend/codeberg4s/CodebergClient.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
package com.worxbend.codeberg4s

import com.worxbend.codeberg4s.client.FutureExec
import com.worxbend.codeberg4s.client.FutureTimer
import com.worxbend.codeberg4s.client.GuardedTelemetry
import com.worxbend.codeberg4s.client.{FutureExec, FutureTimer, GuardedTelemetry}
import com.worxbend.codeberg4s.codec.ApiErrorBodyCodec
import com.worxbend.codeberg4s.core.ApiPipeline
import com.worxbend.codeberg4s.core.BinaryHttpPort
import com.worxbend.codeberg4s.core.Exec
import com.worxbend.codeberg4s.core.Telemetry
import com.worxbend.codeberg4s.core.{ApiPipeline, BinaryHttpPort, Exec, Telemetry}
import com.worxbend.codeberg4s.issues.IssueApi
import com.worxbend.codeberg4s.miscellaneous.MiscellaneousApi
import com.worxbend.codeberg4s.notifications.NotificationApi
import com.worxbend.codeberg4s.organizations.OrganizationApi
import com.worxbend.codeberg4s.paging.PageNumber
import com.worxbend.codeberg4s.paging.PageParams
import com.worxbend.codeberg4s.paging.{PageNumber, PageParams}
import com.worxbend.codeberg4s.pulls.PullRequestApi
import com.worxbend.codeberg4s.repositories.RepositoryApi
import com.worxbend.codeberg4s.repositories.actions.ActionDownloadApi
Expand All @@ -23,8 +17,7 @@ import com.worxbend.codeberg4s.users.UserApi

import sttp.client4.Backend

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}

import java.util.concurrent.atomic.AtomicBoolean

Expand Down
6 changes: 1 addition & 5 deletions modules/client/src/com/worxbend/codeberg4s/VersionApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ package com.worxbend.codeberg4s

import com.worxbend.codeberg4s.client.WireDecode
import com.worxbend.codeberg4s.codec.Json
import com.worxbend.codeberg4s.core.ApiPipeline
import com.worxbend.codeberg4s.core.CodebergRequest
import com.worxbend.codeberg4s.core.Decode
import com.worxbend.codeberg4s.core.Exec
import com.worxbend.codeberg4s.core.RetryEligibility
import com.worxbend.codeberg4s.core.{ApiPipeline, CodebergRequest, Decode, Exec, RetryEligibility}
import com.worxbend.codeberg4s.wire.ServerVersionDto

import scala.concurrent.Future
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.worxbend.codeberg4s.client

import com.worxbend.codeberg4s.CodebergError
import com.worxbend.codeberg4s.CodebergException
import com.worxbend.codeberg4s.core.Exec
import com.worxbend.codeberg4s.{CodebergError, CodebergException}

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.Failure
import scala.util.Success
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

/** The [[com.worxbend.codeberg4s.core.Exec]] instance the published client runs on.
*
Expand Down
26 changes: 12 additions & 14 deletions modules/client/src/com/worxbend/codeberg4s/client/FutureTimer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ package com.worxbend.codeberg4s.client
import com.worxbend.codeberg4s.core.Timer
import com.worxbend.codeberg4s.syntax.discard

import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.duration.Duration
import scala.concurrent.duration.FiniteDuration
import scala.util.Failure
import scala.util.Success
import scala.util.Try

import java.util.concurrent.CancellationException
import java.util.concurrent.Delayed
import java.util.concurrent.RunnableScheduledFuture
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.concurrent.{Future, Promise}
import scala.util.{Failure, Success, Try}

import java.util.concurrent.{
CancellationException,
Delayed,
RunnableScheduledFuture,
ScheduledThreadPoolExecutor,
ThreadFactory,
TimeUnit
}

/** The [[com.worxbend.codeberg4s.core.Timer]] the published client runs on.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.worxbend.codeberg4s.client

import com.worxbend.codeberg4s.CallContext
import com.worxbend.codeberg4s.CodebergError
import com.worxbend.codeberg4s.core.Telemetry
import com.worxbend.codeberg4s.{CallContext, CodebergError}

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.Failure
import scala.util.Success
import scala.util.Try
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}

import java.util.concurrent.ExecutionException

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.worxbend.codeberg4s.client

import com.worxbend.codeberg4s.JsonPath
import com.worxbend.codeberg4s.core.Decode
import com.worxbend.codeberg4s.core.DecodeFailure
import com.worxbend.codeberg4s.core.ResponseBody
import com.worxbend.codeberg4s.core.{Decode, DecodeFailure, ResponseBody}

/** Joins the two halves of reading a response: parse the wire DTO, then project it into the domain.
*
Expand Down
35 changes: 13 additions & 22 deletions modules/client/src/com/worxbend/codeberg4s/issues/IssueApi.scala
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
package com.worxbend.codeberg4s.issues

import com.worxbend.codeberg4s.CodebergError
import com.worxbend.codeberg4s.HttpMethod
import com.worxbend.codeberg4s.Owner
import com.worxbend.codeberg4s.RepoName
import com.worxbend.codeberg4s.core.ApiPipeline
import com.worxbend.codeberg4s.core.CodebergRequest
import com.worxbend.codeberg4s.core.CodebergRequest.bodiless
import com.worxbend.codeberg4s.core.CodebergRequest.read
import com.worxbend.codeberg4s.core.CodebergRequest.remove
import com.worxbend.codeberg4s.core.CodebergRequest.removeWithBody
import com.worxbend.codeberg4s.core.CodebergRequest.write
import com.worxbend.codeberg4s.core.Exec
import com.worxbend.codeberg4s.core.RetryEligibility
import com.worxbend.codeberg4s.issues.wire.CreateIssueCommentOptionDto
import com.worxbend.codeberg4s.issues.wire.CreateIssueOptionDto
import com.worxbend.codeberg4s.issues.wire.CreateLabelOptionDto
import com.worxbend.codeberg4s.issues.wire.EditDeadlineOptionDto
import com.worxbend.codeberg4s.issues.wire.EditIssueOptionDto
import com.worxbend.codeberg4s.issues.wire.IssueMetaDto
import com.worxbend.codeberg4s.issues.wire.IssueQueries
import com.worxbend.codeberg4s.paging.Page
import com.worxbend.codeberg4s.paging.PageParams
import com.worxbend.codeberg4s.core.CodebergRequest.{bodiless, read, remove, removeWithBody, write}
import com.worxbend.codeberg4s.core.{ApiPipeline, CodebergRequest, Exec, RetryEligibility}
import com.worxbend.codeberg4s.issues.wire.{
CreateIssueCommentOptionDto,
CreateIssueOptionDto,
CreateLabelOptionDto,
EditDeadlineOptionDto,
EditIssueOptionDto,
IssueMetaDto,
IssueQueries
}
import com.worxbend.codeberg4s.paging.{Page, PageParams}
import com.worxbend.codeberg4s.{CodebergError, HttpMethod, Owner, RepoName}

import scala.concurrent.Future

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
package com.worxbend.codeberg4s.issues

import com.worxbend.codeberg4s.CodebergError
import com.worxbend.codeberg4s.HttpMethod
import com.worxbend.codeberg4s.Owner
import com.worxbend.codeberg4s.RepoName
import com.worxbend.codeberg4s.core.ApiPipeline
import com.worxbend.codeberg4s.core.CodebergRequest
import com.worxbend.codeberg4s.core.CodebergRequest.read
import com.worxbend.codeberg4s.core.CodebergRequest.remove
import com.worxbend.codeberg4s.core.CodebergRequest.upload
import com.worxbend.codeberg4s.core.CodebergRequest.write
import com.worxbend.codeberg4s.core.Exec
import com.worxbend.codeberg4s.core.RequestBody
import com.worxbend.codeberg4s.core.RetryEligibility
import com.worxbend.codeberg4s.issues.wire.EditAttachmentOptionDto
import com.worxbend.codeberg4s.issues.wire.IssueQueries
import com.worxbend.codeberg4s.core.CodebergRequest.{read, remove, upload, write}
import com.worxbend.codeberg4s.core.{ApiPipeline, CodebergRequest, Exec, RequestBody, RetryEligibility}
import com.worxbend.codeberg4s.issues.wire.{EditAttachmentOptionDto, IssueQueries}
import com.worxbend.codeberg4s.{CodebergError, HttpMethod, Owner, RepoName}

import scala.concurrent.Future

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
package com.worxbend.codeberg4s.issues

import com.worxbend.codeberg4s.CodebergError
import com.worxbend.codeberg4s.HttpMethod
import com.worxbend.codeberg4s.Owner
import com.worxbend.codeberg4s.RepoName
import com.worxbend.codeberg4s.core.ApiPipeline
import com.worxbend.codeberg4s.core.CodebergRequest
import com.worxbend.codeberg4s.core.CodebergRequest.read
import com.worxbend.codeberg4s.core.CodebergRequest.remove
import com.worxbend.codeberg4s.core.CodebergRequest.write
import com.worxbend.codeberg4s.core.Exec
import com.worxbend.codeberg4s.core.RetryEligibility
import com.worxbend.codeberg4s.issues.wire.EditIssueCommentOptionDto
import com.worxbend.codeberg4s.issues.wire.IssueQueries
import com.worxbend.codeberg4s.paging.Page
import com.worxbend.codeberg4s.paging.PageParams
import com.worxbend.codeberg4s.core.CodebergRequest.{read, remove, write}
import com.worxbend.codeberg4s.core.{ApiPipeline, CodebergRequest, Exec, RetryEligibility}
import com.worxbend.codeberg4s.issues.wire.{EditIssueCommentOptionDto, IssueQueries}
import com.worxbend.codeberg4s.paging.{Page, PageParams}
import com.worxbend.codeberg4s.{CodebergError, HttpMethod, Owner, RepoName}

import scala.concurrent.Future

Expand Down
Loading