From 12883345a9af31ee75ca1c8fe1f406c5726e287b Mon Sep 17 00:00:00 2001 From: stivens Date: Fri, 7 Aug 2026 15:32:56 +0200 Subject: [PATCH 1/4] Prepare for 1.0.0: JS/Native cross-build, MiMa, declaration-order eval, publishing hygiene --- .github/workflows/ci.yml | 8 +- README.md | 18 ++- build.sbt | 104 +++++++++--------- project/plugins.sbt | 10 ++ .../stivens/CaseComplete/CaseComplete.scala | 12 +- .../macros/CaseCompleteBuilder.scala | 22 ++-- .../externaluser/ExternalAccessSpec.scala | 7 ++ .../CaseComplete/CaseCompleteSpec.scala | 38 ++++--- 8 files changed, 140 insertions(+), 79 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7422b5a..73a28d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,8 @@ jobs: name: Compile and test CaseComplete runs-on: ubuntu-latest # A LongChainSpec regression fails by hanging, so cap the job instead of burning - # GitHub's 360-minute default. - timeout-minutes: 15 + # GitHub's 360-minute default. Sized for JVM + JS + Native (linking dominates). + timeout-minutes: 30 steps: - uses: actions/checkout@v4 - name: Setup JDK 21, Scala, SBT @@ -27,7 +27,7 @@ jobs: - name: Compile (main & tests) shell: bash run: sbt 'compile; Test / compile' - - name: Unit tests + - name: Unit tests and binary compatibility (MiMa) shell: bash - run: sbt test + run: sbt 'test; mimaReportBinaryIssues' diff --git a/README.md b/README.md index 6bfc8ab..44bff9f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ A Scala 3 library that provides compile-time guarantees for complete case class libraryDependencies += "io.github.stivens" %% "casecomplete" % "0.3.0" ``` +CaseComplete is also published for Scala.js and Scala Native (see [Requirements](#requirements)); in a cross-built project use: + +```scala +libraryDependencies += "io.github.stivens" %%% "casecomplete" % "0.3.0" +``` + `scala-cli`: ```scala @@ -218,7 +224,7 @@ Registers a handler for optional fields, automatically handling `None`: ```scala builder.usingNonEmpty(_.optionalField)(value => transformedValue) -// equivalant to builder.using(_.optionalField)((_: Option[F]).map((value: F) => transformedValue)) +// equivalent to builder.using(_.optionalField)((_: Option[F]).map((value: F) => transformedValue)) ``` #### `ignoring(_.field)` @@ -247,9 +253,19 @@ val result = handler.eval(sourceInstance) // Returns: List[TargetType] ``` +Handlers are evaluated in the declaration order of the source type's fields. Handled fields +that are not primary-constructor fields come last, in the order they were registered. + ## Requirements - Scala >= 3.3 +- Platforms: JVM, Scala.js 1.x, Scala Native 0.5 + +## Compatibility policy + +CaseComplete follows [early semantic versioning](https://www.scala-lang.org/blog/2021/02/16/preventing-version-conflicts-with-versionscheme.html): +binary compatibility is preserved within a major version (post-1.0.0), and every release is +checked against the previous one with [MiMa](https://github.com/lightbend-labs/mima) in CI. ## Contributing diff --git a/build.sbt b/build.sbt index ec66198..94b8c5b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,59 +1,65 @@ import xerial.sbt.Sonatype.sonatypeCentralHost -sonatypeCredentialHost := sonatypeCentralHost - -publishTo := sonatypePublishToBundle.value - -organization := "io.github.stivens" -name := "CaseComplete" -homepage := Some(url("https://github.com/stivens/CaseComplete")) -scmInfo := Some( - ScmInfo( - url("https://github.com/stivens/CaseComplete"), - "scm:git@github.com:stivens/CaseComplete.git" - ) -) -licenses := Seq("MIT" -> url("https://github.com/stivens/CaseComplete/blob/main/LICENSE")) -developers := List( - Developer( - id = "stivens", - name = "Jacek Bizub", - email = "jacekbizub@gmail.com", - url = url("https://github.com/stivens") - ) -) - -version := "0.3.0" - -scalaVersion := "3.3.8" - -resolvers += "shibboleth-releases" at "https://build.shibboleth.net/maven/releases" -resolvers += Resolver.sonatypeCentralSnapshots - -enablePlugins(ScalafixPlugin, SemanticdbPlugin) - inThisBuild( List( - semanticdbEnabled := true + organization := "io.github.stivens", + version := "0.3.0", + scalaVersion := "3.3.8", + homepage := Some(url("https://github.com/stivens/CaseComplete")), + scmInfo := Some( + ScmInfo( + url("https://github.com/stivens/CaseComplete"), + "scm:git@github.com:stivens/CaseComplete.git" + ) + ), + licenses := Seq("MIT" -> url("https://github.com/stivens/CaseComplete/blob/main/LICENSE")), + developers := List( + Developer( + id = "stivens", + name = "Jacek Bizub", + email = "jacekbizub@gmail.com", + url = url("https://github.com/stivens") + ) + ), + versionScheme := Some("early-semver"), + semanticdbEnabled := true, + sonatypeCredentialHost := sonatypeCentralHost ) ) -scalacOptions ++= Seq( - "-Wunused:imports", - "-feature", - "-language:implicitConversions", - "-no-indent", - "-Xmax-inlines", - "128", - "-Xfatal-warnings" -) - -libraryDependencies ++= Seq( - // scalatest - "org.scalactic" %% "scalactic" % "3.2.19", - "org.scalatest" %% "scalatest" % "3.2.19" % "test" -) +lazy val casecomplete = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file(".")) + .settings( + name := "CaseComplete", + // sonatypePublishToBundle is defined per project by sbt-sonatype, so this cannot join + // sonatypeCredentialHost in inThisBuild. + publishTo := sonatypePublishToBundle.value, + scalacOptions ++= Seq( + "-Wunused:imports", + "-feature", + "-language:implicitConversions", + "-no-indent", + "-Xmax-inlines", + "128", + "-Xfatal-warnings" + ), + libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % Test, + // Empty only while the next release is an intentional new binary-compatibility baseline. + // Once 1.0.0 ships, set: Set(organization.value %%% moduleName.value % "1.0.0") + mimaPreviousArtifacts := Set.empty + ) +// The CrossType.Pure platform projects live in ./.jvm, ./.js and ./.native and share ./src; +// this root exists only to aggregate them, so it must not compile ./src itself. lazy val root = project .in(file(".")) - .settings() + .aggregate(casecomplete.jvm, casecomplete.js, casecomplete.native) + .settings( + publish / skip := true, + // Not redundant: MiMa treats an unset mimaPreviousArtifacts as an error (mimaFailOnNoPrevious), + // but an explicitly empty one as "nothing to check". + mimaPreviousArtifacts := Set.empty, + Compile / unmanagedSourceDirectories := Nil, + Test / unmanagedSourceDirectories := Nil + ) diff --git a/project/plugins.sbt b/project/plugins.sbt index 944a2e2..a7a7b36 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,3 +11,13 @@ addSbtPlugin("net.vonbuchholtz" % "sbt-dependency-check" % "5.1.0") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") + +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") + +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") + +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") + +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") diff --git a/src/main/scala/io/github/stivens/CaseComplete/CaseComplete.scala b/src/main/scala/io/github/stivens/CaseComplete/CaseComplete.scala index 17b361c..cae7a0a 100644 --- a/src/main/scala/io/github/stivens/CaseComplete/CaseComplete.scala +++ b/src/main/scala/io/github/stivens/CaseComplete/CaseComplete.scala @@ -3,6 +3,11 @@ package io.github.stivens.casecomplete import io.github.stivens.casecomplete.macros.CaseCompleteBuilder sealed abstract class CaseComplete[SOURCE_TYPE <: Product, TARGET_TYPE] { + + /** + * Applies every registered handler, ordered by the source type's field declaration order. + * Handled fields that are not primary-constructor fields come last, in registration order. + */ def eval(source: SOURCE_TYPE): List[TARGET_TYPE] } @@ -12,11 +17,8 @@ object CaseComplete { } private[casecomplete] class CaseCompleteImpl[SOURCE_TYPE <: Product, TARGET_TYPE]( - handlers: Map[String, SOURCE_TYPE => TARGET_TYPE] + orderedHandlers: List[SOURCE_TYPE => TARGET_TYPE] ) extends CaseComplete[SOURCE_TYPE, TARGET_TYPE] { - private val sortedHandlers: List[SOURCE_TYPE => TARGET_TYPE] = - handlers.toList.sortBy(_._1).map(_._2) - def eval(source: SOURCE_TYPE): List[TARGET_TYPE] = - sortedHandlers.map(_(source)) + orderedHandlers.map(_(source)) } diff --git a/src/main/scala/io/github/stivens/CaseComplete/macros/CaseCompleteBuilder.scala b/src/main/scala/io/github/stivens/CaseComplete/macros/CaseCompleteBuilder.scala index 7c1cd1a..a371cad 100644 --- a/src/main/scala/io/github/stivens/CaseComplete/macros/CaseCompleteBuilder.scala +++ b/src/main/scala/io/github/stivens/CaseComplete/macros/CaseCompleteBuilder.scala @@ -32,6 +32,10 @@ class CaseCompleteBuilder[SOURCE_TYPE <: Product, TARGET_TYPE, Handled <: Tuple] private[casecomplete] def markHandled[NewHandled <: Tuple]: CaseCompleteBuilder[SOURCE_TYPE, TARGET_TYPE, NewHandled] = new CaseCompleteBuilder(handlers) + // Fields ignored via `ignoring` appear in fieldOrder but have no handler, hence the flatMap. + private[casecomplete] def orderedHandlers(fieldOrder: List[String]): List[SOURCE_TYPE => TARGET_TYPE] = + fieldOrder.flatMap(handlers.get) + // `using`, `usingNonEmpty` and `ignoring` must stay methods on the class. A `transparent inline` // extension method binds its receiver to a parameter proxy carrying the refined type of the whole // preceding chain, which makes compiling a chain exponential in its length -- see LongChainSpec. @@ -216,9 +220,9 @@ object CaseCompleteBuilder { import q.reflect.* val handledFields = getHandledFields[Handled] - val caseClassFields = TypeRepr.of[SOURCE_TYPE].typeSymbol.caseFields.map(_.name).toSet + val caseClassFields = TypeRepr.of[SOURCE_TYPE].typeSymbol.caseFields.map(_.name) - val missingFields = caseClassFields -- handledFields + val missingFields = caseClassFields.diff(handledFields) if missingFields.nonEmpty then report.errorAndAbort(s""" |CaseComplete compilation failed: Missing handlers for ${missingFields.size} field(s) in class ${Type.show[SOURCE_TYPE]}. @@ -232,7 +236,9 @@ object CaseCompleteBuilder { | // ... other handlers | .compile""") - '{ new CaseCompleteImpl($builder.handlers) } + val fieldOrder = caseClassFields ++ handledFields.diff(caseClassFields) + + '{ new CaseCompleteImpl($builder.orderedHandlers(${ Expr(fieldOrder) })) } } // Decoded structurally rather than with quoted type patterns ('[head *: tail]): every chain step @@ -240,15 +246,17 @@ object CaseCompleteBuilder { // typer time at 96 fields. Unlike those patterns this decodes only the literal `*:` spine of // ConstantTypes that registerField emits, not Tuple2-sugar shapes -- safe because the constructor // and markHandled are package-private, so nothing else produces a Handled. - private def getHandledFields[Handled <: Tuple: Type](using q: Quotes): Set[String] = { + // Returns the fields in registration order (earliest first): the tuple is built by prepending, + // so prepending again while walking head-to-tail restores the original order. + private def getHandledFields[Handled <: Tuple: Type](using q: Quotes): List[String] = { import q.reflect.* val consSymbol = TypeRepr.of[Any *: Tuple].typeSymbol val emptyTupleSymbol = TypeRepr.of[EmptyTuple].dealias.typeSymbol - def loop(repr: TypeRepr, acc: Set[String]): Set[String] = repr.dealias match { + def loop(repr: TypeRepr, acc: List[String]): List[String] = repr.dealias match { case AppliedType(tycon, List(ConstantType(StringConstant(name)), tail)) if tycon.typeSymbol == consSymbol => - loop(tail, acc + name) + loop(tail, name :: acc) case empty if empty.typeSymbol == emptyTupleSymbol => acc case other if other.typeSymbol.isAbstractType => report.errorAndAbort( @@ -257,6 +265,6 @@ object CaseCompleteBuilder { case other => report.errorAndAbort(s"Internal error: unexpected Handled type: ${other.show}") } - loop(TypeRepr.of[Handled], Set.empty) + loop(TypeRepr.of[Handled], Nil) } } diff --git a/src/test/scala/externaluser/ExternalAccessSpec.scala b/src/test/scala/externaluser/ExternalAccessSpec.scala index 652c203..ae033a2 100644 --- a/src/test/scala/externaluser/ExternalAccessSpec.scala +++ b/src/test/scala/externaluser/ExternalAccessSpec.scala @@ -64,6 +64,13 @@ class ExternalAccessSpec extends AnyFunSpec with CompileErrorAssertions { "handlers" ) } + + it("should not expose the ordered handlers") { + assertInaccessible( + """CaseComplete.build[Filter, Option[String]].orderedHandlers(List("a", "b"))""", + "orderedHandlers" + ) + } } private inline def assertInaccessible(inline code: String, member: String): Unit = diff --git a/src/test/scala/io/github/stivens/CaseComplete/CaseCompleteSpec.scala b/src/test/scala/io/github/stivens/CaseComplete/CaseCompleteSpec.scala index 5ff3e1a..364443a 100644 --- a/src/test/scala/io/github/stivens/CaseComplete/CaseCompleteSpec.scala +++ b/src/test/scala/io/github/stivens/CaseComplete/CaseCompleteSpec.scala @@ -3,24 +3,26 @@ package io.github.stivens.casecomplete import org.scalatest.funspec.AnyFunSpec import testsupport.CompileErrorAssertions -import java.time.Year - class CaseCompleteSpec extends AnyFunSpec with CompileErrorAssertions { describe("CaseCompleteBuilder") { describe("when given a source type and a target type") { + // Ints rather than java.time.Year and Double: the JS and Native javalibs have no java.time, + // and Scala.js renders the Double 7.0 as "7", so either would leak the platform into the + // expected strings. case class MovieFilter( title_like: Option[String] = None, director_eq: Option[String] = None, - releaseYear_eq: Option[Year] = None, - rating_gte: Option[Double] = None + releaseYear_eq: Option[Int] = None, + rating_gte: Option[Int] = None ) val filter = MovieFilter( - releaseYear_eq = Some(Year.of(1999)), - rating_gte = Some(7.0) + releaseYear_eq = Some(1999), + rating_gte = Some(7) ) - val expectedOrder = List("rating >= 7.0", "releaseYear = 1999") + // Declaration order (releaseYear_eq before rating_gte); alphabetical order would swap them. + val expectedOrder = List("releaseYear = 1999", "rating >= 7") val expectedResult = expectedOrder.toSet val buildMovieFilterHandler = CaseComplete.build[MovieFilter, Option[String]] @@ -51,7 +53,7 @@ class CaseCompleteSpec extends AnyFunSpec with CompileErrorAssertions { assert(evaulated == expectedResult) } - it("should evaluate handlers in alphabetical order of field name, on every call") { + it("should evaluate handlers in field declaration order, on every call") { // Repeated so that caching the ordering as something single-use (a view, an iterator) fails here. assert(movieFilterHandler.eval(filter).flatten == expectedOrder) assert(movieFilterHandler.eval(filter).flatten == expectedOrder) @@ -76,10 +78,11 @@ class CaseCompleteSpec extends AnyFunSpec with CompileErrorAssertions { case class MovieFilter( title_like: Option[String] = None, director_eq: Option[String] = None, - releaseYear_eq: Option[Year] = None, - rating_gte: Option[Double] = None + releaseYear_eq: Option[Int] = None, + rating_gte: Option[Int] = None ) { lazy val isEmpty: Boolean = this == MovieFilter.empty + val extra: String = "extra" val foo: String = "bar" } @@ -101,12 +104,21 @@ class CaseCompleteSpec extends AnyFunSpec with CompileErrorAssertions { assert(true) // code compiles } - it("should allow the extra fields to be handled") { - val movieFilterHandler = allConstructorFieldsHandled + it("should evaluate extra-field handlers after the constructor fields, in registration order") { + // `foo` is registered first though `extra` precedes it both alphabetically and in + // declaration order, and both are registered before any constructor field: only + // "constructor fields first, then extras in registration order" yields this output. + val movieFilterHandler = CaseComplete + .build[MovieFilter, Option[String]] .using(_.foo)(Some(_)) + .using(_.extra)(Some(_)) + .using(_.title_like)(_ => Some("title")) + .using(_.director_eq)(_ => None) + .using(_.releaseYear_eq)(_ => None) + .using(_.rating_gte)(_ => None) .compile - assert(movieFilterHandler.eval(MovieFilter()).flatten == List("bar")) + assert(movieFilterHandler.eval(MovieFilter()).flatten == List("title", "bar", "extra")) } } From 6abc825b13fd503f3e11345a466650e7c8c85e57 Mon Sep 17 00:00:00 2001 From: stivens Date: Thu, 1 Oct 2026 15:06:37 +0200 Subject: [PATCH 2/4] Migrate build to sbt 2.x and upgrade dependencies --- .github/workflows/ci.yml | 11 +++++++++-- .scalafmt.conf | 2 +- build.sbt | 26 +++++++++++++------------- project/build.properties | 2 +- project/plugins.sbt | 22 ++++++++-------------- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73a28d4..db87169 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,22 @@ jobs: # GitHub's 360-minute default. Sized for JVM + JS + Native (linking dominates). timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup JDK 21, Scala, SBT - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' cache: 'sbt' - name: Setup SBT uses: sbt/setup-sbt@v1 + # setup-java's sbt cache covers dependencies only, not sbt 2's content-addressed task cache. + - name: Cache sbt 2 task outputs + uses: actions/cache@v6 + with: + path: ~/.cache/sbt/v2 + key: sbt2-${{ runner.os }}-${{ hashFiles('build.sbt', 'project/**', 'src/**') }} + restore-keys: sbt2-${{ runner.os }}- - name: Compile (main & tests) shell: bash run: sbt 'compile; Test / compile' diff --git a/.scalafmt.conf b/.scalafmt.conf index 8c8daa8..fd7786f 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.9.3 +version = 3.11.5 align.preset = more // For pretty alignment. importSelectors = singleLine maxColumn = 140 // For my wide 30 display. diff --git a/build.sbt b/build.sbt index 94b8c5b..280996d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,18 +1,16 @@ -import xerial.sbt.Sonatype.sonatypeCentralHost - inThisBuild( List( organization := "io.github.stivens", version := "0.3.0", scalaVersion := "3.3.8", homepage := Some(url("https://github.com/stivens/CaseComplete")), - scmInfo := Some( + scmInfo := Some( ScmInfo( url("https://github.com/stivens/CaseComplete"), "scm:git@github.com:stivens/CaseComplete.git" ) ), - licenses := Seq("MIT" -> url("https://github.com/stivens/CaseComplete/blob/main/LICENSE")), + licenses := Seq("MIT" -> url("https://github.com/stivens/CaseComplete/blob/main/LICENSE")), developers := List( Developer( id = "stivens", @@ -21,9 +19,8 @@ inThisBuild( url = url("https://github.com/stivens") ) ), - versionScheme := Some("early-semver"), - semanticdbEnabled := true, - sonatypeCredentialHost := sonatypeCentralHost + versionScheme := Some("early-semver"), + semanticdbEnabled := true ) ) @@ -31,10 +28,8 @@ lazy val casecomplete = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file(".")) .settings( - name := "CaseComplete", - // sonatypePublishToBundle is defined per project by sbt-sonatype, so this cannot join - // sonatypeCredentialHost in inThisBuild. - publishTo := sonatypePublishToBundle.value, + name := "CaseComplete", + publishTo := localStaging.value, scalacOptions ++= Seq( "-Wunused:imports", "-feature", @@ -44,11 +39,16 @@ lazy val casecomplete = crossProject(JVMPlatform, JSPlatform, NativePlatform) "128", "-Xfatal-warnings" ), - libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % Test, + libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.20" % Test, // Empty only while the next release is an intentional new binary-compatibility baseline. - // Once 1.0.0 ships, set: Set(organization.value %%% moduleName.value % "1.0.0") + // Once 1.0.0 ships, set: Set(organization.value %% moduleName.value % "1.0.0") mimaPreviousArtifacts := Set.empty ) + .nativeSettings( + // test-interface declares a strict scheme, but Scala Native keeps 0.5.x binary compatible; + // without this, scalatest's older 0.5.x pin is a fatal eviction under sbt 2. + libraryDependencySchemes += "org.scala-native" % s"test-interface_${ScalaNativePlatform}_${scalaBinaryVersion.value}" % VersionScheme.Always + ) // The CrossType.Pure platform projects live in ./.jvm, ./.js and ./.native and share ./src; // this root exists only to aggregate them, so it must not compile ./src itself. diff --git a/project/build.properties b/project/build.properties index c02c575..ddca111 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.3 +sbt.version=2.0.9 diff --git a/project/plugins.sbt b/project/plugins.sbt index a7a7b36..daa4611 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,23 +1,17 @@ -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.7.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.3") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.9") -addDependencyTreePlugin +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.2") -addSbtPlugin("net.vonbuchholtz" % "sbt-dependency-check" % "5.1.0") - -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") - -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") - -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.4.0") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.4.0") -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.2.1") From ea9544b4ab4363696670018ecb8a71846ccc2ce8 Mon Sep 17 00:00:00 2001 From: stivens Date: Thu, 1 Oct 2026 15:13:18 +0200 Subject: [PATCH 3/4] fix --- .github/workflows/ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db87169..d0b4d98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,6 @@ jobs: cache: 'sbt' - name: Setup SBT uses: sbt/setup-sbt@v1 - # setup-java's sbt cache covers dependencies only, not sbt 2's content-addressed task cache. - - name: Cache sbt 2 task outputs - uses: actions/cache@v6 - with: - path: ~/.cache/sbt/v2 - key: sbt2-${{ runner.os }}-${{ hashFiles('build.sbt', 'project/**', 'src/**') }} - restore-keys: sbt2-${{ runner.os }}- - name: Compile (main & tests) shell: bash run: sbt 'compile; Test / compile' From 77151e1c3ca827b8360fc5547d7d76c4f318bec1 Mon Sep 17 00:00:00 2001 From: stivens Date: Thu, 1 Oct 2026 15:23:52 +0200 Subject: [PATCH 4/4] 1.0.0 --- README.md | 8 ++++---- build.sbt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 44bff9f..968f9d2 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,25 @@ A Scala 3 library that provides compile-time guarantees for complete case class `build.sbt`: ```scala -libraryDependencies += "io.github.stivens" %% "casecomplete" % "0.3.0" +libraryDependencies += "io.github.stivens" %% "casecomplete" % "1.0.0" ``` CaseComplete is also published for Scala.js and Scala Native (see [Requirements](#requirements)); in a cross-built project use: ```scala -libraryDependencies += "io.github.stivens" %%% "casecomplete" % "0.3.0" +libraryDependencies += "io.github.stivens" %%% "casecomplete" % "1.0.0" ``` `scala-cli`: ```scala -//> using lib "io.github.stivens::casecomplete:0.3.0" +//> using lib "io.github.stivens::casecomplete:1.0.0" ``` `scala-cli REPL`: ```bash -scala-cli repl --dep io.github.stivens::casecomplete:0.3.0 +scala-cli repl --dep io.github.stivens::casecomplete:1.0.0 ``` ## Quick Start diff --git a/build.sbt b/build.sbt index 280996d..7a796e5 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ inThisBuild( List( organization := "io.github.stivens", - version := "0.3.0", + version := "1.0.0", scalaVersion := "3.3.8", homepage := Some(url("https://github.com/stivens/CaseComplete")), scmInfo := Some(