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 7 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,21 @@
package smithy4s.example

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

final case class UnwrappedValidatedFoo(name: Option[String] = None)

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

val hints: Hints = Hints.empty

implicit val schema: Schema[UnwrappedValidatedFoo] = struct(
UnwrappedValidatedString.underlyingSchema.optional[UnwrappedValidatedFoo]("name", _.name),
){
UnwrappedValidatedFoo.apply
}.withId(id).addHints(hints)
}
@@ -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 UnwrappedValidatedString extends Newtype[String] {
val id: ShapeId = ShapeId("smithy4s.example", "UnwrappedValidatedString")
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[UnwrappedValidatedString] = 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: Option[ValidatedString] = None)

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.optional[ValidatedFoo]("name", _.name),
){
ValidatedFoo.apply
}.withId(id).addHints(hints)
}
@@ -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 ValidatedString extends Newtype[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, asBijection)
}
Expand Up @@ -41,6 +41,7 @@ package object example {
type Weather[F[_]] = smithy4s.kinds.FunctorAlgebra[WeatherGen, F]
val Weather = WeatherGen

type UnwrappedValidatedString = smithy4s.example.UnwrappedValidatedString.Type
type PublishersList = smithy4s.example.PublishersList.Type
type TestString = smithy4s.example.TestString.Type
type ArbitraryData = smithy4s.example.ArbitraryData.Type
Expand All @@ -60,6 +61,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
@@ -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("unwraped string test"))}"
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
Expand Up @@ -19,11 +19,12 @@ package smithy4s.codegen.internals
import cats.~>

import Type.Alias
import Type.Nullable
import Type.PrimitiveType
import Type.ValidatedAlias
import TypedNode._
import Type.ExternalType
import LineSegment._
import smithy4s.codegen.internals.Type.Nullable

private[internals] object CollisionAvoidance {
def apply(compilationUnit: CompilationUnit): CompilationUnit = {
Expand Down Expand Up @@ -86,6 +87,14 @@ private[internals] object CollisionAvoidance {
rec,
hints.map(modHint)
)
case ValidatedTypeAlias(shapeId, name, tpe, recursive, hints) =>
ValidatedTypeAlias(
shapeId,
protectKeyword(name.capitalize),
modType(tpe),
recursive,
hints.map(modHint)
)
case Enumeration(shapeId, name, tag, values, hints) =>
val newValues = values.map {
case EnumValue(value, intValue, name, realName, hints) =>
Expand Down Expand Up @@ -128,6 +137,8 @@ private[internals] object CollisionAvoidance {
val protectedName = protectKeyword(name.capitalize)
val unwrapped = isUnwrapped | (protectedName != name.capitalize)
Alias(namespace, protectKeyword(name.capitalize), modType(tpe), unwrapped)
case ValidatedAlias(namespace, name, tpe) =>
ValidatedAlias(namespace, protectKeyword(name.capitalize), modType(tpe))
case PrimitiveType(prim) => PrimitiveType(prim)
case ExternalType(name, fqn, typeParams, pFqn, under, refinementHint) =>
ExternalType(
Expand Down Expand Up @@ -215,6 +226,8 @@ private[internals] object CollisionAvoidance {
)
case NewTypeTN(ref, target) =>
NewTypeTN(modRef(ref), target)
case ValidatedNewTypeTN(ref, target) =>
ValidatedNewTypeTN(modRef(ref), target)
case AltTN(ref, altName, alt) =>
AltTN(modRef(ref), altName, alt)
case MapTN(values) =>
Expand Down Expand Up @@ -301,6 +314,7 @@ private[internals] object CollisionAvoidance {
val EnumValue_ = NameRef("smithy4s.schema", "EnumValue")
val EnumTag_ = NameRef("smithy4s.schema", "EnumTag")
val Newtype_ = NameRef("smithy4s", "Newtype")
val NewtypeValidated_ = NameRef("smithy4s", "NewtypeValidated")
val Hints_ = NameRef("smithy4s", "Hints")
val ShapeTag_ = NameRef("smithy4s", "ShapeTag")
val ErrorSchema_ = NameRef("smithy4s.schema", "ErrorSchema")
Expand Down
14 changes: 14 additions & 0 deletions modules/codegen/src/smithy4s/codegen/internals/IR.scala
Expand Up @@ -102,6 +102,14 @@ private[internals] case class TypeAlias(
hints: List[Hint] = Nil
) extends Decl

private[internals] case class ValidatedTypeAlias(
shapeId: ShapeId,
name: String,
tpe: Type,
recursive: Boolean = false,
hints: List[Hint] = Nil
) extends Decl

private[internals] case class Enumeration(
shapeId: ShapeId,
name: String,
Expand Down Expand Up @@ -279,6 +287,8 @@ private[internals] object Type {
tpe: Type,
isUnwrapped: Boolean
) extends Type
case class ValidatedAlias(namespace: String, name: String, tpe: Type)
extends Type
case class PrimitiveType(prim: Primitive) extends Type
case class ExternalType(
name: String,
Expand Down Expand Up @@ -422,6 +432,8 @@ private[internals] object TypedNode {
fields.traverse(_.traverse(_.traverse(f))).map(StructureTN(ref, _))
case NewTypeTN(ref, target) =>
f(target).map(NewTypeTN(ref, _))
case ValidatedNewTypeTN(ref, target) =>
f(target).map(ValidatedNewTypeTN(ref, _))
case AltTN(ref, altName, alt) =>
alt.traverse(f).map(AltTN(ref, altName, _))
case MapTN(values) =>
Expand Down Expand Up @@ -452,6 +464,8 @@ private[internals] object TypedNode {
fields: List[(String, FieldTN[A])]
) extends TypedNode[A]
case class NewTypeTN[A](ref: Type.Ref, target: A) extends TypedNode[A]
case class ValidatedNewTypeTN[A](ref: Type.Ref, target: A)
extends TypedNode[A]
case class AltTN[A](ref: Type.Ref, altName: String, alt: AltValueTN[A])
extends TypedNode[A]
case class MapTN[A](values: List[(A, A)]) extends TypedNode[A]
Expand Down