Skip to content

Commit

Permalink
Adds a way to get packed inputs in operations (#122)
Browse files Browse the repository at this point in the history
* Adds a way to get packed inputs in operations

* Adds a way for users to tweak rendering of services methods, and
substitute a list of parameters in methods for a single case-class
parameter (which smithy4s uses under the hood anyway), corresponding to
the smithy shape of the operation's input (when present). This might
offer better UX in some occurences.
* Adds documentation

* Change test name

* adoptium => temurin

* Update modules/docs/src/04-codegen/01-customisation.md

Co-authored-by: Jeff Lewis <[email protected]>

Co-authored-by: Jeff Lewis <[email protected]>
  • Loading branch information
Baccata and lewisjkl committed Feb 22, 2022
1 parent 1d64b0d commit bb9a66b
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 8 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest]
java: [adoptium@17]
java: [temurin@17]
scalaVersion: ["2_12", "2_13", "3_0"]
scalaPlatform: ["jvm", "js"]
ceVersion: ["CE2", "CE3"]
Expand Down Expand Up @@ -98,7 +98,7 @@ jobs:
- name: Setup Java and Scala
uses: olafurpg/setup-scala@v13
with:
java-version: adoptium@17
java-version: temurin@17

- name: Cache
uses: coursier/cache-action@v6
Expand Down
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ lazy val core = projectMatrix
(ThisBuild / baseDirectory).value / "sampleSpecs" / "empty.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "product.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "weather.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "discriminated.smithy"
(ThisBuild / baseDirectory).value / "sampleSpecs" / "discriminated.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "packedInputs.smithy"
),
(Test / sourceGenerators) := Seq(genSmithyScala(Test).taskValue),
testFrameworks += new TestFramework("weaver.framework.CatsEffect")
Expand Down
1 change: 1 addition & 0 deletions modules/codegen/src/smithy4s/codegen/IR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ sealed trait Hint
object Hint {
case object Trait extends Hint
case object Error extends Hint
case object PackedInputs extends Hint
case class Protocol(traits: List[Type.Ref]) extends Hint
// traits that get rendered generically
case class Native(typedNode: Fix[TypedNode]) extends Hint
Expand Down
49 changes: 49 additions & 0 deletions modules/codegen/src/smithy4s/codegen/PostProcessor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2021 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.codegen

trait PostProcessor extends (CompilationUnit => CompilationUnit) {}

object PostProcessor extends PostProcessor {

val all: List[PostProcessor] = List(PackedInputsShift)

def apply(unit: CompilationUnit): CompilationUnit = {
all.foldLeft(unit)((acc, f) => f(acc))
}

}

object PackedInputsShift extends PostProcessor {
def apply(unit: CompilationUnit): CompilationUnit = {
val newDecls = unit.declarations.map {
case s: Service => transformService(s)
case other => other
}
unit.copy(declarations = newDecls)
}

def transformService(s: Service): Service = {
if (s.hints.contains(Hint.PackedInputs)) {
val newOps = s.ops.map { op =>
op.copy(hints = Hint.PackedInputs :: op.hints)
}
s.copy(ops = newOps)
} else s
}

}
12 changes: 10 additions & 2 deletions modules/codegen/src/smithy4s/codegen/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
ops.map {
case op if op.input == Type.unit =>
s"def ${op.methodName}(${op.renderArgs}) = ${op.name}()"
case op if op.hints.contains(Hint.PackedInputs) =>
s"def ${op.methodName}(${op.renderArgs}) = ${op.name}(input)"
case op =>
s"def ${op.methodName}(${op.renderArgs}) = ${op.name}(${op.input.render}(${op.renderParams}))"
}
Expand All @@ -230,6 +232,8 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
ops.map {
case op if op.input == Type.unit =>
s"case ${op.name}() => impl.${op.methodName}(${op.renderParams})"
case op if op.hints.contains(Hint.PackedInputs) =>
s"case ${op.name}(input) => impl.${op.methodName}(${op.renderParams})"
case op =>
s"case ${op.name}(${op.input.render}(${op.renderParams})) => impl.${op.methodName}(${op.renderParams})"

Expand Down Expand Up @@ -539,11 +543,15 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
private implicit class OperationExt(op: Operation) {
def renderArgs =
if (op.input == Type.unit) ""
else self.renderArgs(op.params)
else if (op.hints.contains(Hint.PackedInputs)) {
"input: " + renderInput
} else self.renderArgs(op.params)

def renderParams =
if (op.input == Type.unit) ""
else op.params.map(_.name).mkString(", ")
else if (op.hints.contains(Hint.PackedInputs)) {
"input"
} else op.params.map(_.name).mkString(", ")

def methodName = uncapitalise(op.name)

Expand Down
14 changes: 11 additions & 3 deletions modules/codegen/src/smithy4s/codegen/SmithyToIR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package smithy4s.codegen
import cats.data.NonEmptyList
import cats.implicits._
import smithy4s.recursion._
import smithy4s.meta.PackedInputsTrait
import software.amazon.smithy.aws.traits.ServiceTrait
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.node.Node
Expand All @@ -31,7 +32,9 @@ import scala.jdk.CollectionConverters._
object SmithyToIR {

def apply(model: Model, namespace: String): CompilationUnit = {
CompilationUnit(namespace, new SmithyToIR(model, namespace).allDecls)
PostProcessor(
CompilationUnit(namespace, new SmithyToIR(model, namespace).allDecls)
)
}

private[codegen] def prettifyName(
Expand Down Expand Up @@ -373,12 +376,17 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) {
Type.Ref(shapeId.getNamespace(), shapeId.getName())
)
Hint.Protocol(refs.toList)
case _: PackedInputsTrait =>
Hint.PackedInputs
case t if t.toShapeId() == ShapeId.fromParts("smithy.api", "trait") =>
Hint.Trait
}

private def traitsToHints(traits: List[Trait]): List[Hint] =
traits.collect(traitToHint) ++ traits.map(unfoldTrait)
private def traitsToHints(traits: List[Trait]): List[Hint] = {
val nonMetaTraits =
traits.filterNot(_.toShapeId().getNamespace() == "smithy4s.meta")
traits.collect(traitToHint) ++ nonMetaTraits.map(unfoldTrait)
}

implicit class ShapeExt(shape: Shape) {
def name = shape.getId().getName()
Expand Down
32 changes: 32 additions & 0 deletions modules/core/test/src/smithy4s/PackedInputsSmokeSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2021 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 weaver._
import cats.Id
import smithy4s.example.{PackedInputsService, PackedInput}

object PackedInputsSmokeSpec extends FunSuite {

test("Methods with packed inputs have a single case-class parameter") {
val service: PackedInputsService[Id] = new PackedInputsService[Id] {
def packedInputOperation(input: PackedInput): Unit = ()
}
expect.same(service.packedInputOperation(PackedInput(key = "foo")), ())
}

}
64 changes: 64 additions & 0 deletions modules/docs/src/04-codegen/01-customisation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
sidebar_label: Protocols and smithy4s
title: Protocols and smithy4s
---

Smithy4s is opinionated in what the generated code look like, there are a few things that can be tweaked.

#### Packed inputs

By default, smithy4s generates methods the parameters of which map to the fields of the input structure of the corresponding operation.

For instance :

```kotlin
service PackedInputsService {
version: "1.0.0",
operations: [PackedInputOperation]
}

operation PackedInputOperation {
input: PackedInput,
}

structure PackedInput {
@required
a: String,
@required
b: String
}
```

leads to something conceptually equivalent to :

```scala
trait PackedInputServiceGen[F[_]] {

def packedInputOperation(a: String, b: String) : F[Unit]

}
```

It is however possible to annotate the service (or operation) definition with the `smithy4s.meta#packedInputs` trait, in order for the rendered method to contain a single parameter, typed with actual input case class of the operation.

For instance :

```scala
use smithy4s.meta#packedInputs

@packedInputs
service PackedInputsService {
version: "1.0.0",
operations: [PackedInputOperation]
}
```

will produce the following Scala code

```scala  
trait PackedInputServiceGen[F[_]] {

def packedInputOperation(input: PackedInput) : F[Unit]

}
```
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ smithy4s.api.SimpleRestJsonTrait$Provider
smithy4s.api.UncheckedExamplesTrait$Provider
smithy4s.api.UuidFormatTrait$Provider
smithy4s.api.DiscriminatedUnionTrait$Provider
smithy4s.meta.PackedInputsTrait$Provider
1 change: 1 addition & 0 deletions modules/protocol/resources/META-INF/smithy/manifest
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
smithy4s.smithy
smithy4s.meta.smithy
15 changes: 15 additions & 0 deletions modules/protocol/resources/META-INF/smithy/smithy4s.meta.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$version: "1.0"

metadata suppressions = [
{
id: "UnreferencedShape",
namespace: "smithy4s.meta",
reason: "This is a library namespace."
}
]


namespace smithy4s.meta

@trait(selector: ":is(service, operation)")
structure packedInputs {}
47 changes: 47 additions & 0 deletions modules/protocol/src/smithy4s/meta/PackedInputsTrait.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2021 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.meta;

import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.AnnotationTrait;
import software.amazon.smithy.model.traits.AbstractTrait;

public class PackedInputsTrait extends AnnotationTrait {

public static ShapeId ID = ShapeId.from("smithy4s.meta#packedInputs");

public PackedInputsTrait(ObjectNode node) {
super(ID, node);
}

public PackedInputsTrait() {
super(ID, Node.objectNode());
}

public static final class Provider extends AbstractTrait.Provider {
public Provider() {
super(ID);
}

@Override
public PackedInputsTrait createTrait(ShapeId target, Node node) {
return new PackedInputsTrait(node.expectObjectNode());
}
}
}
18 changes: 18 additions & 0 deletions sampleSpecs/packedInputs.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace smithy4s.example

use smithy4s.meta#packedInputs

@packedInputs
service PackedInputsService {
version: "1.0.0",
operations: [PackedInputOperation]
}

operation PackedInputOperation {
input: PackedInput,
}

structure PackedInput {
@required
key: String
}

0 comments on commit bb9a66b

Please sign in to comment.