Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validated newtypes #1454

Open
wants to merge 27 commits into
base: series/0.19
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dffd10d
Add NewtypeValidated
denisrosca Mar 6, 2024
c477541
Fix formatting
denisrosca Mar 19, 2024
468d54c
Add simple renderer tests
denisrosca Mar 19, 2024
c600b1f
Fix validated newtype usage
denisrosca Mar 19, 2024
ba9b81b
Render validated newtypes only if smithy4sRenderValidatedNewtypes=true
denisrosca Mar 19, 2024
301ed39
Fix formatting again
denisrosca Mar 19, 2024
cf9ec8e
Don't validate types from smithy.api namespace
denisrosca Mar 25, 2024
de82030
Minor fixup of sbt plugin test
denisrosca Mar 25, 2024
cd48f63
Add validated newtypes usage to generated metadata
denisrosca Apr 8, 2024
89f3911
Merge remote-tracking branch 'upstream/series/0.19' into validated-ne…
denisrosca Apr 8, 2024
043c859
Fix newline
denisrosca Apr 8, 2024
be7b3e1
Fix missing license header
denisrosca Apr 9, 2024
7ec86d6
Add ValidatedNewtypesTransformer and switch to meta trait
denisrosca Apr 10, 2024
e6db5e1
Add default value to bootstrapped sample
denisrosca Apr 10, 2024
4f7b313
Fix renderer spec
denisrosca Apr 10, 2024
54762fe
Add transformer spec
denisrosca Apr 10, 2024
c00ed39
Fix formatting
denisrosca Apr 10, 2024
5ff681c
Add Validator interface and refactor constraint validation
denisrosca Apr 18, 2024
5f27413
Rename trait
denisrosca Apr 18, 2024
382b96a
Fix scala3 encoding
denisrosca Apr 18, 2024
a09a568
Fix formatting, again
denisrosca Apr 18, 2024
9fe555a
Add doc entry
denisrosca Apr 18, 2024
e378c0a
Make mdoc compile only
denisrosca Apr 18, 2024
1e2b2ea
Format mandatory transformers
denisrosca Apr 24, 2024
c98a563
Make Validator trait sealed
denisrosca Apr 24, 2024
e7ca985
Add changelog entry
denisrosca Apr 24, 2024
d38247d
Change unsafeApply to throw exceptions
denisrosca Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,15 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Newtype
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.schema.Schema.bijection
import smithy4s.schema.Schema.string

object NonValidatedString extends Newtype[String] {
val id: ShapeId = ShapeId("smithy4s.example", "NonValidatedString")
val hints: Hints = Hints.empty
val underlyingSchema: Schema[String] = string.withId(id).addHints(hints).validated(smithy.api.Length(min = Some(1L), max = None)).validated(smithy.api.Pattern("[a-zA-Z0-9]+"))
implicit val schema: Schema[NonValidatedString] = bijection(underlyingSchema, asBijection)
}
@@ -0,0 +1,21 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.schema.Schema.struct

final case class ValidatedFoo(name: ValidatedString = smithy4s.example.ValidatedString.unsafeApply("abc"))

object ValidatedFoo extends ShapeTag.Companion[ValidatedFoo] {
val id: ShapeId = ShapeId("smithy4s.example", "ValidatedFoo")

val hints: Hints = Hints.empty

implicit val schema: Schema[ValidatedFoo] = struct(
ValidatedString.schema.field[ValidatedFoo]("name", _.name).addHints(smithy.api.Default(smithy4s.Document.fromString("abc"))),
){
ValidatedFoo.apply
}.withId(id).addHints(hints)
}
@@ -0,0 +1,18 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.NewtypeValidated
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.Validator
import smithy4s.schema.Schema.bijection
import smithy4s.schema.Schema.string

object ValidatedString extends NewtypeValidated[String] {
val id: ShapeId = ShapeId("smithy4s.example", "ValidatedString")
val hints: Hints = Hints.empty
val underlyingSchema: Schema[String] = string.withId(id).addHints(hints).validated(smithy.api.Length(min = Some(1L), max = None)).validated(smithy.api.Pattern("[a-zA-Z0-9]+"))
implicit val schema: Schema[ValidatedString] = bijection(underlyingSchema, asBijectionUnsafe)
val validator: Validator[String, ValidatedString] = Validator.of[String, ValidatedString].validating(smithy.api.Length(min = Some(1L), max = None)).validating(smithy.api.Pattern("[a-zA-Z0-9]+")).build
@inline def apply(a: String): Either[String, ValidatedString] = validator.validate(a)
}
Expand Up @@ -60,6 +60,7 @@ package object example {
type MapWithMemberHints = smithy4s.example.MapWithMemberHints.Type
type TestIdRefList = smithy4s.example.TestIdRefList.Type
type CitySummaries = smithy4s.example.CitySummaries.Type
type ValidatedString = smithy4s.example.ValidatedString.Type
/** This is a simple example of a "quoted string" */
type AString = smithy4s.example.AString.Type
type NonEmptyMapNumbers = smithy4s.example.NonEmptyMapNumbers.Type
Expand Down Expand Up @@ -91,6 +92,7 @@ package object example {
type TestStructurePattern = smithy4s.example.TestStructurePattern.Type
type UnicodeRegexString = smithy4s.example.UnicodeRegexString.Type
type UVIndex = smithy4s.example.UVIndex.Type
type NonValidatedString = smithy4s.example.NonValidatedString.Type
@deprecated(message = "N/A", since = "N/A")
type Strings = smithy4s.example.Strings.Type
type NonEmptyCandies = smithy4s.example.NonEmptyCandies.Type
Expand Down
96 changes: 96 additions & 0 deletions modules/bootstrapped/test/src/smithy4s/ValidatedNewtypesSpec.scala
@@ -0,0 +1,96 @@
/*
* Copyright 2021-2023 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package smithy4s

import smithy4s.schema.Schema.bijection
import smithy4s.schema.Schema.string

class ValidatedNewtypesSpec() extends munit.FunSuite {
val id1 = "id-1"
val id2 = "id-2"

test("Validated newtypes are consistent") {
expect.same(AccountId.unsafeApply(id1).value, id1)
expect.different(
AccountId.unsafeApply(id1).value,
AccountId.unsafeApply(id2).value
)
expect.different(
implicitly[ShapeTag[AccountId]],
implicitly[ShapeTag[DeviceId]]
)
expect.same(AccountId.unapply(AccountId.unsafeApply(id1)), Some(id1))
}

test("Newtypes have well defined unapply") {
val aid = AccountId.unsafeApply(id1)
aid match {
case AccountId(id) => expect(id == id1)
}
}

type DeviceId = DeviceId.Type
object DeviceId extends NewtypeValidated[String] {

val id: ShapeId = ShapeId("foo", "DeviceId")
val hints: Hints = Hints.empty

val underlyingSchema: Schema[String] = string
.withId(id)
.addHints(hints)
.validated(smithy.api.Length(min = Some(1L), max = None))

implicit val schema: Schema[DeviceId] =
bijection(underlyingSchema, asBijectionUnsafe)

val validator: Validator[String, DeviceId] = Validator
.of[String, DeviceId]
.validating(smithy.api.Length(min = Some(1L), max = None))
.build

@inline def apply(a: String): Either[String, DeviceId] =
validator.validate(a)

}

type AccountId = AccountId.Type

object AccountId extends NewtypeValidated[String] {
def id: smithy4s.ShapeId = ShapeId("foo", "AccountId")
val hints: Hints = Hints.empty

val underlyingSchema: Schema[String] = string
.withId(id)
.addHints(hints)
.validated(smithy.api.Length(min = Some(1L), max = None))
.validated(smithy.api.Pattern("[a-zA-Z0-9]+"))

implicit val schema: Schema[AccountId] =
bijection(underlyingSchema, asBijectionUnsafe)

val validator: Validator[String, AccountId] = Validator
.of[String, AccountId]
.validating(smithy.api.Length(min = Some(1L), max = None))
.validating(smithy.api.Pattern("[a-zA-Z0-9]+"))
.build

@inline def apply(a: String): Either[String, AccountId] =
validator.validate(a)

}

}
@@ -0,0 +1,11 @@
lazy val root = (project in file("."))
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
scalaVersion := "2.13.10",
Compile / smithy4sRenderValidatedNewtypes := true,
libraryDependencies ++= Seq(
"com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion.value,
"com.disneystreaming.smithy4s" %% "smithy4s-dynamic" % smithy4sVersion.value,
"com.disneystreaming.alloy" % "alloy-core" % "0.3.4",
)
)
@@ -0,0 +1 @@
sbt.version=1.8.3
@@ -0,0 +1,9 @@
sys.props.get("plugin.version") match {
case Some(x) =>
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % x)
case _ =>
sys.error(
"""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin
)
}
@@ -0,0 +1,38 @@
/*
* Copyright 2021-2024 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package newtypes.validated

import newtypes.validated._

object Main extends App {
try {
val cityOrError: Either[String, ValidatedCity] = ValidatedCity("test-city")
val nameOrError: Either[String, ValidatedName] = ValidatedName("test-name")
val country: String = "test-country"

println(
(nameOrError, cityOrError) match {
case (Right(name), Right(city)) => s"Success: ${Person(name, Some(city), Some(country))}"
case _ => s"Error"
}
)
} catch {
case _: java.lang.ExceptionInInitializerError =>
println("failed")
sys.exit(1)
}
}
@@ -0,0 +1,26 @@
namespace newtypes.validated

use smithy4s.meta#unwrap
use alloy#simpleRestJson

@length(min: 1, max: 10)
string ValidatedCity

@length(min: 1, max: 10)
string ValidatedName

@unwrap
@length(min: 1, max: 10)
string ValidatedCountry

structure Person {
@httpLabel
@required
name: ValidatedName

@httpQuery("town")
town: ValidatedCity

@httpQuery("country")
country: ValidatedCountry
}
@@ -0,0 +1,2 @@
# check if smithy4sCodegen works and everything compiles
> compile
Expand Up @@ -16,8 +16,9 @@

package smithy4s.codegen

import sbt._
import sbt.Keys._
import sbt._

import Smithy4sCodegenPlugin.autoImport._

private final case class SmithyBuildData(
Expand Down
Expand Up @@ -19,7 +19,10 @@ package smithy4s.codegen
import sbt.Keys._
import sbt.util.CacheImplicits._
import sbt.{fileJsonFormatter => _, _}
import scala.util.{Success, Try}

import scala.util.Success
import scala.util.Try

import JsonConverters._

object Smithy4sCodegenPlugin extends AutoPlugin {
Expand Down Expand Up @@ -138,6 +141,11 @@ object Smithy4sCodegenPlugin extends AutoPlugin {
"Boolean value to indicate whether or not to generate optics"
)

val smithy4sRenderValidatedNewtypes =
taskKey[Boolean](
"Boolean value to indicate whether or not to generate validated newtypes"
)

val smithy4sGeneratedSmithyFiles =
taskKey[Seq[File]](
"Generated smithy files"
Expand Down Expand Up @@ -252,36 +260,51 @@ object Smithy4sCodegenPlugin extends AutoPlugin {
}
},
config / smithy4sRenderOptics := false,
config / smithy4sRenderValidatedNewtypes := false,
config / smithy4sGeneratedSmithyMetadataFile := {
(config / sourceManaged).value / "smithy" / "generated-metadata.smithy"
},
config / smithy4sGeneratedSmithyFiles := {
val cacheFactory = (config / streams).value.cacheStoreFactory
val cached = Tracked.inputChanged[(String, Boolean), Seq[File]](
cacheFactory.make("smithy4sGeneratedSmithyFilesInput")
) { case (changed, (wildcardArg, shouldGenerateOptics)) =>
val lastOutput = Tracked.lastOutput[Boolean, Seq[File]](
cacheFactory.make("smithy4sGeneratedSmithyFilesOutput")
) { case (changed, prevResult) =>
if (changed || prevResult.isEmpty) {
val file = (config / smithy4sGeneratedSmithyMetadataFile).value
IO.write(
file,
s"""$$version: "2"
|metadata smithy4sWildcardArgument = "$wildcardArg"
|metadata smithy4sRenderOptics = $shouldGenerateOptics
|""".stripMargin
)
Seq(file)
} else {
prevResult.get
val cached =
Tracked
.inputChanged[(String, Boolean, Boolean), Seq[File]](
cacheFactory.make("smithy4sGeneratedSmithyFilesInput")
) {
case (
changed,
(
wildcardArg,
shouldGenerateOptics,
shouldRenderValidatedNewtypes
)
) =>
val lastOutput = Tracked.lastOutput[Boolean, Seq[File]](
cacheFactory.make("smithy4sGeneratedSmithyFilesOutput")
) { case (changed, prevResult) =>
if (changed || prevResult.isEmpty) {
val file =
(config / smithy4sGeneratedSmithyMetadataFile).value
IO.write(
file,
s"""$$version: "2"
|metadata smithy4sWildcardArgument = "$wildcardArg"
|metadata smithy4sRenderOptics = $shouldGenerateOptics
|metadata smithy4sRenderValidatedNewtypes = $shouldRenderValidatedNewtypes
|""".stripMargin
)
Seq(file)
} else {
prevResult.get
}
}
lastOutput(changed)
Comment on lines +274 to +301
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I'd extract some of this to avoid the current nesting level, which is becoming a bit unwieldy

}
}
lastOutput(changed)
}
val wildcardArg = (config / smithy4sWildcardArgument).value
val generateOptics = (config / smithy4sRenderOptics).value
cached((wildcardArg, generateOptics))
val renderValidatedNewtypes =
(config / smithy4sRenderValidatedNewtypes).value
cached((wildcardArg, generateOptics, renderValidatedNewtypes))
},
config / sourceGenerators += (config / smithy4sCodegen).map(
_.filter(_.ext == "scala")
Expand Down
@@ -1,3 +1,4 @@
smithy4s.codegen.transformers.AwsStandardTypesTransformer
smithy4s.codegen.transformers.AwsConstraintsRemover
smithy4s.codegen.transformers.OpenEnumTransformer
smithy4s.codegen.transformers.ValidatedNewtypesTransformer