microsoft / microsoft/typespec
Usage decorator (Input/Output/InputOutput)
- Dominant language
- Java
- Stars
- 5.9k
- Forks
- 394
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 104
Description
# Input and Output in TypeSpec
Various emitters and protocol would benefit from a way to differentiate between input and output.
- GraphQL has different constraint for input and output types and providing a way to explicitly annoate a type as input or output would be useful.
- Various Azure client emitters currently use Input and output to generate different client class(An outpout only model wouldn't have constructor generated)
Things to take into account:
- We can clearly establish a what is used as input, output or both by looking at operation parameters and return type. We already provide an api to do so. `function resolveUsages(container: Namespace | Interface | Operation): UsageTracker;`
- Usage could be affected by visibility(A readonly property being the only reference to a model wouldn't mark that model as an input). This means this might be different depending on the protocol.
- Do we need a more generic usage system? Like tracking a type is used in a certain payload kind(e.g. Json vs Xml) - tcgc does this
- We most likely want to error out when incompatible input or output are used.(e.g. an Input only model reference an output only model)
- How does input vs output affect emitter that don't involve operations(e.g. json schema)
- Could something be an input only in a protocol but output/input in another.
## Single marker for input or output
In this case we could use `in`/`out` keyword which would result in a clean syntax.
```tsp
@secret
in scalar password extends string;
```
It makes it quite simple to figure out that we have some incompatible reference here
```tsp
out model UserRead { password: password }
^ error password is an input only type but is used in a output only type
```
It might become a bit more tricky when referenced from `inout` types. Do we automatically infer that properties, union variants that reference an input only or output only type should be omitted.
This now bleeds right into the visibility design where we expect properties/union variants to be explicitly annotated.
```tsp
inout model User {
id: UserId; // do we understand that this property is not part of the User input model
password: password;
firstName: string;
}
out model UserId {
company: string;
name: string;
}
```
We could also consider that an error. i.e. you can only reference type of the same usage kind(input only from input only, output only from output only and input/output from both).
This is however quite limiting when working with visibility and render this incompatible.
## So why not just use visibility
Input/Output still differ from visibility. Input could have many different visibility(create, update, etc.)
## Pass a protocol to a `@usage` decorator
Let suppose we have the following operations
```tsp
@scope(Sql) // not a decorator today, just the idea
namespace DataBase;
op setUserFromDb(user: User): void;
op getUserFromDb(): User;
@scope(Http)
namespace FE;
op getUser(): User;
op setUser(user: User): void; // error user is set as input only for http protocol
```
We could define the following:
```tsp
@usage(Usage.Input | Usage.Output, Sql)
@usage(Usage.Output, Http)
model User {
firstName: string;
@usage(Usage.Output, Http) id: UserId;
// ^ error UserId is output only but used as input in sql
@usage(Usage.Input, Http) password: string;
// ^ error only specified as output for http
}
@usage(Usage.Output) model UserId { domain: string, name: string }
```
## This looks a lot like visibility
This now looks awefully similar to visibility but with more limitation. Visibility is able to specify different kind for input or output (create, update,etc.).
So now if we take the example above and we just want to make password available in create
```tsp
@usage(Usage.Input | Usage.Output, Sql)
@usage(Usage.Input, Http)
model User {
firstName: string;
@usage(Usage.Input, Http) @visibility(HttpVis.Create) password: string;
}
```
Now this is a bit tedious I need to specify things twice. We should be able to map visibility to input or output so those get applied automatically which would allow the following.
```tsp
@usage(Usage.Input | Usage.Output, Sql)
@usage(Usage.Input, Http)
model User {
firstName: string;
@visibility(HttpVis.Create) password: string;
}
```
And then why do we not have `@visibility` on the model itself
```tsp
@visibility(SqlVis)
@visibility(HttpVis.Create, HttpVis.Update)
model User {
firstName: string;
@visibility(HttpVis.Create) password: string;
}
```
At this point this just really feels like a more limited syntax/syntactic sugar for setting all the input vs output visibilities
## Input/Output as a global flag and visibility to control protocol specificness
So this comes to the last option which is to keep visibility as the protocol specific decider and having `in`/`out`/`inout` keyword or `@usage` decorator but something that applies in all cases.
Taking the example above
```tsp
out model UserId { domain: string, name: string }
inout model User {
@visibility(HttpVis.read) id: UserId;
@visibility(HttpVis.create) password: string;
}
```
With the code above we either wouldn't be able to validate which defeats a little the point of having those modifier or we'd need to duplicate things and have visibility just be a way to be more precise for a protocol
```tsp
out model UserId { domain: string, name: string }
inout model User {
out id: UserId;
@visibility(HttpVis.create) in password: string;
@visibility(HttpVis.read) in other: string;
// ^ error cannot apply a readonly visibility to a in property
}
```
## How does this work with protocol agnostic emitters
One of the thing this could ideally solve is having the ability to mark `readonly`, `writeonly` in json schema for example.
In the case of using a global switch this seems quite straight forward, `in`/`out` can map to `readonly`, `writeonly`.
In the case of some protocol specific switch json schema would need to be able to choose which protocol to generate the schema from(or use the default value as the protocol was optional). This feels inline with the fact that it should probably also respect visibility(e.g. if generating json schema for models of an http service)
## keyword vs decorator
I think a general thing we want for typespec is have the ability to use some decorator to specify some syntax sugar like `@default` or `@optional`, etc. So i believe it would be safer to start with a decorator and move to a syntax later if it is widely used. There is 2 options:
```tsp
@in
@out
@inout
```
```tsp
@usage(Usage.in)
@usage(Usage.out)
@usage(Usage.Input | Usage.Output)
```
The first one would be a cleaner syntax specially if it doesn't take a protocol
Contributor guide
Assessment
This issue has not been assessed yet.