JSII provides the tools for software to invoke and interact with with a Javascript runtime. JSII provides this with models generated from exported Javascript, and a RPC protocol. The JSII model defines the JSII types that language binding must code generated JSII proxies for. The code generated proxies wrap calls to the JSII runtime via a JSII RPC client. The JSII RPC client must be implemented by the language binding for communicating with the JSII runtime through the JSII RPC.
CDK is the only Javascript tool with JSII cross language bindings. This may change in the future, but JSII was purpose built for the CDK usecase. The JSII for Go implementation should be able to support an other Javascript tool with JSII cross language bindings.
JSII is a JSON document IDL defining classes and interfaces that language bindings are generated from. JSII modeled types are classes and interfaces. These types are built on subtyping inheritance, and polymorphic Object Oriented concepts. Binding languages are required to use these concepts in their language, or if the language does not have these concepts, simulate them to the best means possible.
The Go programming language is type system is not based on object oriented and polymorphic principles. Instead, Go is a procedural language of types which may contain methods, and duck-typed interfaces. In addition, concepts such as class subtyping are not directly supported by the language's type system. These concepts can be simulated in go, but this comes at a unexpected and non-idiomatic experience cost for the customer.
All JSII classes and datatype interfaces must be rendered into two Go types, an interface and struct. The two types are required to facilitate subtyping of JSII types and class inheritance. Go structs do not support subtyping. The struct provides the concrete implementation of the JSII class. The interface provides support for subtyping the struct's value as its extended type, or implemented interfaces.
JSII requires language bindings to support subtyping. Subtyping is the concept
where a subtype implements or extend another base type, and that subtype can be
used anywhere the base type is accepted, (e.g. Foo
extends Bar
, so Foo
must be usable as a parameter anywhere Bar
is).
Given Go's type system, only interfaces can satisfy this requirement. In order for a Go type to be used as a subtype of another Go type, the target type must be defined as an interface. There are no alternatives to this requirement. This requirement, combined with class inheritance and polymorphism requirements place significant limitations and restrictions how Go proxy types can be defined.
JSII types are defined with a Javascript first approach. This means that all JSII types are based heavily on Javascript type system. To translate these JSII types into Go proxy types, care must be taken to ensure the generated Go proxy types are forward compatible with the underling Javascript types. In addition, the JSII for Go generation must take care to limit or reduce the risk of identifier name clashing. Due to the differences in language philosophy between Javascript and Go. The Go proxy types must generate additional identifiers into the namespace that risk clashing with JSII types. The additional identifier names generated must also be forward compatible with future additions made to the underling Javascript types.
JSII Unions are defined as collections of immutable string values with the enum type as the namespace. Go does not have direct support for enumerations, but does have common idiom for defining a collection of immutable values of scalar types, (e.g. string, float, bool, etc). The Go idiom for enums does not have namespacing though. This means enum values are global to the Go package they are defined within.
Without the namespace the enum value identifier name have a significant risk to name clashing with other JSII types. To address this risk the JSII for Go generator must render enum value's name with the namespace included. This technique is the idiomatic naming convention of enum values in Go.
There are two different ways the JSII for Go could define enum value identifier names, EnumValue_EnumType or EnumValueEnumType. The enum value's identifier name would or would not use a underscore() separating the enum value and enum type identifier sections. With the underscore there is very little to no risk of identifier name clashing, but this is not an idiomatic style. Go idiomatic naming does not include underscores().
type Color string
const(
Red Color Color = "Red"
Harlequin_Color Color = "Harlequin"
TropicalRainForest_Color Color = "Tropical rain forest"
)
JSII interfaces are similar to Go's duck-typed interfaces. The JSII interface is a collection of methods and property getter/setters that can be satisfied by a JSII class.
JSII interfaces identifier names can be rendered as Go types without any modifications.
type IConstruct interface{
GetFoo() string
SomeMethod(a string)
}
Note: JSII interfaces, and JSII datatype interfaces are very different.
JSII datatype interfaces are similar to JSII classes with the exception that
properties are exported struct members, regardless of the immutability
flag.
A JSII interface that extends another interface when rendered in Go must the extended interface be an anonymous member of the Go sub interface in addition to the methods of the sub interface.
It is invalid in Go for an embedded interface to include a method with the same name as a method in the interface where it is embedded. Two sibling interfaces may define the same method as long all definitions are the same.
For example the IFoo
JSII interface extends IBar
JSII interface. The
following is the resulting Go types.
type IBar interface{ /* */ }
type IFoo interface{
IBar
/* */
}
JSII language bindings are proxies for the underlying JSII RPC class, Go structs representing the JSII class must only export methods
The only exception to this is JSII datatype interface. Datatype interfaces are equivalent to structs or typed dictionaries where the customer provides the member values directly. The datatype interfaces are not JSII proxies.
JSII properties and method parameters can be decorated as optional. This
decoration means that a value for the parameter or property is not needed. In
Javascript this means a undef
value is allowed. Parameters decorated with
optional in JSII for Go must use a pointer type when rendering the parameter.
Users of JSII for Go would leave properties unset, and use nil
for method
parameters that are optional.
If a JSII class, datatype interface, or interface has a property that decorated
with immutable
a getter method must be generated on the type for property.
The method must be prefixed with Get
to distinguish it from the member name,
if any.
type StatePropsIface interface {
GetComment() *string
}
type StateProps struct {
Comment *string
}
func (s *StateProps) GetComment() *string { return s.Comment }
If a JSII class, datatype interface, or interface has a property that not
decorated with immutable
a setter method must be generated on the type for
the property. The method must be prefixed with Set
to distinguish it from
the member name, if any.
type StatePropsIface interface {
GetComment() *string
SetComment() *string
}
type StateProps struct {
Comment *string
}
func (s *StateProps) GetComment() *string { return s.Comment }
func (s *StateProps) SetComment(v string) { return s.Comment }
JSII classes support polymorphic inheritance and subtyping. The JSII for Go must render both a Go interface and struct for every one JSII class in order to satisfy this requirement. Go structs do not support subtyping. The struct provides the concrete implementation of the JSII class. The interface provides support for subtyping the Struct's value as its extended type, or implemented interfaces.
To ensure subtyping is supported by all Go JSII proxy types, all occurrences of the JSII class in property type and method input/output parameters must use the Go interface type.
Even if the JSII class does not extend another JSII class, the JSII for Go generator must always generate both Go interface and struct types. The reason for this is to ensure the JSII for Go generated code will be forward compatible, and it ensures customers will be able to write custom Go types that extend JSII classes.
The generated Go interface must embed all extended JSII class's Go interfaces as anonymous fields. Likewise the Go struct for a JSII class must include the extended JSII class's Go struct as anonymous field. This pattern ensures the Go types correctly satisfy the simulated polymorphic requirements.
The Go interface for a JSII class must include a private method in its definition to prevent users of the JSII for Go from providing custom implementations for the JSII proxy. Users of the JSII for Go will still be able to extend JSII classes, but would not be able to provide custom implementations. This design requirement ensures that user code will not be broken when JSII classes add methods or properties to the underlying Javascript classes.
In order to prevent name classing the Go interface must be suffixed with an
identifier that separates it from the Go struct. For example, an Iface
suffix
provides this separation.
// Fail provides the subtyping interfaces for JSII Fail class.
type FailIface interface {
StateIface
faiPrivate()
}
// Fail is a JSII class.
type Fail struct {
*State
base jsii.Base
}
func (*Fail) fail_private() {}
Removing the private method from the Go interface would allow users to provide
custom implementations for JSII classes, and would remove the need for the Go
struct to be generated for each JSII class. While this would remove the overall
type confusion it opens the user up to breaking changes or unexpected behavior.
If the Go struct is remove the interface's identifier name does not need to be
suffixed with Iface
.
TODO: fill out this section with reasoning about why the three constructors are needed.
New<ClassName>
- Public constructor the user would use to create a new instance of this JSII proxy type.InternalNew<ClassName>AsBaseClass
- Internally used by the JSII for Go generated proxy types to initialize extended JSII classes.Extend<ClassName>
- Public constructor for user to associate their custom Go type's methods as overriding methods defined by the JSII proxy type. This allows customers to override JSII class methods.
JSII classes may be defined with static methods and constant properties. Both of these translate to package global Go functions. Similar to JSII enums, JSII static methods and constant properties must be rendered with the JSII class's name included in the identifier name of the Go function.
The static methods and constant property Go functions could suffix the JSII class name to the method name, or separate the two names with an underscore(_). Similar to JSII enums, using the underscore separator ensures the resulting identifier name will always be forward compatible, and not clash with other JSII identifiers, but at the cost of not being idiomatic Go types.
func ClassName_StaticMethodName(...) T { ... }
JSII datatype interfaces are a collection of property members on a datatype. This correlates closely to Go's struct type. JSII datatype interfaces are very different from JSII interfaces not labeled as datatype. JSII datatype interfaces are a collection of properties that can be extended with other JSII datatype interfaces.
The datatype interface's properties can be decorated as abstract
. This
decoration has no meaningful value. All datatype interface properties in Go
must be represented as struct members.
Similar to JSII classes, datatype interface must generate two Go types, an interface, and struct for each JSII datatype interface. Like JSII classes two types are generated to support subtyping. JSII datatype interfaces are always rendered as two Go type, even when a datatype interface does not extend another datatype interface. This is done to ensure forward compatibility if a JSII datatype interface were to extend another datatype interface in the future.
type StatePropsIface interface {
GetComment() *string
GetInputPath() *string
}
type StateProps struct {
Comment *string
InputPath *string
}
Unlike the JSII class the JSII datatype interface's Go interface does not need to define a private method to restrict implementation. There is no reason to limit the custom implementation of the datatype interface's proxy as this type is only a collection of properties.
Promise do not exist in Go. In Go synchronous API function are idiomatic APIs. JSII functions retuning a would be wrapped within a synchronous Go function that wait for the async JSII function to be complete. The synchronous go function would then return the result or error. This is the same pattern used for Java and .Net JSII bindings.
Unlike most JSII functions, functions returning Promises will return both the functions modeled return type, and an error.
func AnAsyncJSIIFunction() (T, error) { /* ... */ }
JSII Unions are defined on a type's reference, not as their own type. Implementation of Unions in Go would require a type to be defined, and named. JSII Unions are anonymous and the JSII for Go would have to derive a sustainable name for the union that does not clash with other types in the JSII model.
Unions should not be supported at all, or if they are required to be supported,
JSII Unions must translate to empty interface{}
. There is no other
sustainable way anonymous unions could be implemented in Go.
JSII includes both maps and arrays. Both of these types translate easily to Go
types. An JSII array is a Go slice []T
. A JSII map is a Go map,
map[string]T
. JSII a map must only have a key of a string
type. T
is the
JSII modeled elementType
.
JSII Date would be satisfied by Go's time.Time
type.
JSII String would be satisfied by Go's string
type.
JSII number does not distinguish the difference between integer and float. This
either requires JSII for Go to treat all numbers as float64
, or define a new
jsii.Number
type that "wraps" Go number types into the singular JSII Number
type.
Typescript numbers are float64 with a max integer size of 2^53 -1. This means
that Customers could want to provide a int64
value that would lose precision.
The closet analogy to JSII's json
PrimitiveType
is a
map[string]interface{}
. While the map does allow the user to retrieve
arbitrary fields from the map, the user is required to cast the retrieved value
to a Go type before it can be used. This process can intensive for users, and
an area for code bugs.
v := failState.ToStateObject()
f, ok := v["foo"]
if !ok {
// handle field not set
}
ft, ok := f.(jsii.Number)
if !ok {
// handle field not a number
}
// Use ft as a jsii.Number type
JSII any maps to Go's empty interface type. JSII for Go should replace all JSII
any
types with interface{}
JSII requires that language bindings must ensure only a single instance of the JSII runtime is executing per invocation of the app built with the JSII language bindings. For JSII for Go this translates to a global instance of the JSII client value that all Go proxy types generated from the JSII model will reference.
JSII has no method for defining errors, or exceptions that can be thrown. Regardless if an exception is a recoverable exception or not. In addition to not defining errors, the JSII RPC makes no distinction between RPC errors, kernel runtime errors, and errors encountered with the underlying Javascript code.
This limitation forces the JSII for Go into a poor customer experience for exposing errors to customers. Either all Go JSII proxies return error in addition to their modeled return type, or all JSII proxies panic if a JSII exception is received. Without JSII including modeling or customizations on top of the models there are no other options. Both of these solutions are poor customer experiences with panic being the least bad since customers will not need to check for error on every possible function, method, and property call.
Using JSII for Go outside of CDK's specific all errors are unrecoverable, and singleton design is most likely unusable due to the lack of meaningful errors defined within JSII.
The JSII runtime "kernel" is executed within a NodeJS subprocess. The JSII RPC client must communicate with the NodeJS subprocess using stdin/stdout pipes.
JSII's pacmac
should be extended to generate Go code from the JSII model.
While JSII is a JSON document there are implicit complexities reading the
format that would incur a high maintenance cost implementing a JSII for Go
generator in Go instead of using JSII's built in pacmac
. The pacmac
is a
typescript implementation reading JSII model and JSII language binding code
generation. There doesn't seem to be a strong reason why a Go pacmac
should
be written instead of adding JSII for Go generation to the existing pacmac
.