Hint Bindings
Introduced in Smithy4s version 0.18.43 as opt-in behavior. Without opting in to this behavior, your generated code will remain the same as before.
Configuration
Trait Based
If you wish to render dynamic hint bindings, you can do so with a Smithy Trait called smithy4s.meta#renderAsDynamicBinding.
$version: "2"
namespace test
use smithy4s.meta#renderAsDynamicBinding
@trait()
@renderAsDynamicBinding
structure myTrait {}
@myTrait()
structure Test {}
With the above example, any shape using test#myTrait, such as test#Test, will render the Hint for this trait as a dynamic binding rather than a static one. This does not change the rendering of test#myTrait itself.
Note, it is likely that you will want to use the smithy apply syntax to apply this trait rather than putting it directly on the trait. You may also want to use a smithy-level transformation to automatically apply the trait to trait definitions from certain namespaces.
Metadata Based
If you want to enable dynamic hint bindings for an entire namespace, you can do so with the smithy4sRenderDynamicHintNamespacePatterns metadata key. For example, if we want to render all instances of traits from foo namespace as dynamic, we'd set the following:
metadata smithy4sRenderDynamicHintNamespacePatterns = ["foo.*"]
Note that this will set any namespaces under foo as dynamic as well. So foo.bar, for example, would also be rendered as dynamic, but food would not. You can use other patterns, consider the following examples:
a.b.c- exact match, will match onlya.b.ca.b.*- will matcha.bfollowed with some segments.a.b*- like above, but will also matcha.bora.bc
Justification
The reason for this option is that static hint bindings can cause incompatibility issues when upstream Smithy models (Smithy Prelude, Alloy) change. These issues can pop up because of the fact that Smithy4s code-generates Scala code for these Smithy shapes and packages them as part of Smithy4s Core as opposed to code-generating them at the same time it generates code for the end-user Smithy shapes. This is necessary because the modules of Smithy4s use these shapes to drive their functionality. They need to "know" about these shapes/traits ahead of time in order to build interpreters that work using them. However, when using static hint bindings, there is room for divergence between what was generated by Smithy4s and published in smithy4s-core and what the end user application is generating.
Example 1
For example, say smithy4s generated the following static Hint as part of smithy4s-core:
case class Foo(a: String)
Now, if that trait Foo receives an update in Smithy to, for example, add a new optional field b of type Integer, it can cause an issue. Specifically if the end-user is using a version of smithy4s that is older (made prior to this trait being changed to add field b), but pulling in an artifact to codegen with that is using a newer version of the same trait Foo. When trying to code-generate, we'll get a compile error:
val hints = Hints(Foo(a = "test", b = Some(2)))
This will not compile because Smithy4s did not yet generate a new case class for Foo which contains the field b.
Example 2
Another case where this can be an issue is if a new trait is added entirely. Say we add a new trait Bar to Alloy. This can cause the same issue. If smithy4s has not yet generated Bar as a possible Hint, then when the user generates code that uses Bar, it will try to reference this case class that doesn't exist.
val hints = Hints(Bar()) // compilation error, Bar does not exist
Dynamic Hint Bindings
Dynamic hint bindings are a solution that helps get around the issues above and opens up more flexible options for how hints/traits are evolved.
Dynamic hint bindings use a generic representation of a trait and its data as opposed to a custom, code-generated one. Dynamic hints contain two pieces of information:
- The ShapeId of the trait the Hint is representing
- A Document data structure that represents the data of the trait
Let's look again at the examples above and show how dynamic hint bindings solve the issue.
Example 1
In example 1 above, we ran into an issue with adding a new field which had not yet been generated as part of smithy4s-core. With dynamic hint bindings, this is not an issue:
val hints = Hints(Hints.dynamic(ShapeId("alloy", "Foo"), smithy4s.Document.obj("a" -> Document.fromString("test"), "b" -> Document.fromInt(2))))
This will not cause any compilation errors and from the perspective of smithy4s, it will convert this DynamicBinding into a static binding where it will effectively just drop b since it doesn't yet know about it.
Example 2
In example 2 above, there was a compilation failure because we were referencing a type that did not yet exist. With dynamic bindings we avoid this issue as well:
val hints = Hints(Hints.dynamic(ShapeId("alloy", "Bar"), smithy4s.Document.obj()))
Since we are not referencing the generated type directly, we avoid a compilation error.