Skip to content

Instantly share code, notes, and snippets.

@johanste
Last active September 28, 2022 18:37
Show Gist options
  • Save johanste/26230aa2fc2612673c1e0289576656ee to your computer and use it in GitHub Desktop.
Save johanste/26230aa2fc2612673c1e0289576656ee to your computer and use it in GitHub Desktop.
Client coden annotations for Cadl

Cadl for code gen

Requirements

  • There should be no need for customizations. A "raw" service specification should be enough to generate a client library.

  • The cost of customizations should be proportional to the amount of customizations we are making. It must not have a steep cost cliff when we want to customize a small aspect of the generated code (a.k.a. "the cost of the first customization should be low").

Solution

A set of client code generation decorators and operation templates are introduced into a @azure-tools/cadl-dpg library.

For minor, targeted customizations, augment (@@) decorators in a side car file can be used.

For more extensive customizations, a side car file with a parallel set of interfaces that cherry-pick operations can be used.

The following rules apply when interpreting a service specification for client code generation purposes:

  • Unless an explicit @client is defined, there is always an implicit global default client instance to which each operation group and method is expected to be associated with.

    This allows for an empty side-car file.

  • If there is a single @client defined, it acts as the default client instance, and all operation groups and methods are associated with it unless overridden.

  • If there are multiple @client instances, operation groups and methods have to be explicitly associated with the client instance using @extendsClient or being members of a client interface.

Public Decorators

Public decorators are intended for usage by service description authors and client customization side-cars.

@Dpg.client(target: Interface, apiVersions?: Enum)

Designates the given interface as a client. The apiVersions parameter indicates which API service API versions should be used to generate code.

@Dpg.operationGroup(target: Interface | Operation, group: string)

Marks the given interface or operation as part of an operation group. If applied to an interface, it will apply to all methods in the interface unless there the operation is explicitly marked with @operationGroup (most specific marking wins).

@Dpg.extendsClient(target: Operation | Interface, extends: Interface)

Marks the given operation or interface as being a member of the provided client interface. Requires the extends interface to have been marked with @Dpg.client. Typical usage is as an augment decorator from a side-car file.

Public Operation templates

Constructor

A Constructor operation indicates that client code generators are expected to provide an affordance to create/initialize a client using the given signature.

ClientAccessor<TParams, TClient>

A ClientAccessor operation indcates that an operation as a (sub) client accessor. Client code generators are expected to provide an affordance to, from the given (parent) client access a sub/child client.

Private decorators

The private decorators are generally intended for marking the operation templates. Service spec authors to not use the private decorators, but emitters will use the corresponding accessors to access marked entities.

@Dpg.Private.constructor(target: Operation)

Marks an operation as a constructor. Applied to the Constructor<TParams> operation template.

@Dpg.Private.clientAccessor(target: Operation)

Marks an operation as a (sub) client accessor. Applied to the ClientAccessor<TParams, TClient> operation template

Required core language features

  • Augmentation decorator (@@)
  • Ability to reference all elements that can be decorated (e.g. operation parameters, return value)

Examples

Minor customizations

// main.cadl

namespace Azure.Stuff;

op oneOp(): void;
op twoOp(): string;

interface Lifetime {
  op create(): Thing;
  op delete(): void;
}

client.cadl:

@@operationGroup("things", Lifetime)

Resulting Python codegen:

class ThingsOperationGroup:
  def create(self) -> Thing: ...
  def delete(self) -> None: ...

class Client:
  things: ThingsOperationGroup
  def one_op(self) -> None: ...
  def two_op(self) -> str: ...

Two clients

main.cadl

op create(): Model;
op train(): void;
op infer(modelId: string): string;

client.cadl

import "./main.cadl";

@Dpg.client
interface AdminClient {
  createModel is create;
  trainModel is train;

  getInferrenceClient is Dpg.ClientAccessor<{modelId: string}, ModelInferrenceClient>;
}

@Dpg.client
interface ModelInferrenceClient {

  constructor is Dpg.Constructor<{modelId: string}>;

  // Issue; does it matter that the signature should change/client level
  // params are excluded? Too much magic?
  infer is infer;
}

Resulting Python codegen from cadl compile client.cadl

class AdminClient:
  def create_model(self) -> Thing: ...
  def train_model(self) -> None: ...

  def get_inferrence_client(self) -> ModelInferrenceClient: ...

class ModelInferrenceClient:

  def __init__(self, model_id: string): ...
  def infer(self) -> string: ...

Compiling (generating) a client

cadl compile client.cadl

Design issues

  • The interaction between client constructor parameters and their mapping/binding to operation parameters is currently implicit (e.g. the parameters should "move" from operations to the client constructor). We could require linking of parameters from the constructor to operations, but that would be very verbose.

  • How to associate operation group with client. Operation template "accessor"?

  • (Minor) misspelling an operation group name introduces a new operation group.

FAQ

  • How do we specify the version/tell the emitter which version set (enum) to enumerate over.
    • Provide version enum to client decorator.
@markcowl
Copy link

markcowl commented Sep 28, 2022

Notes from cadl/dpg sync:

  • Make examples more like portions of an actual data plane spec
  • More explicit definitions of clients versus sub clients, perhaps different entities for each
    • ensure that entity-names for Client, SubClient, and OperationGroup match the vocabulary used in guidelines
  • Provide an explicit example of default client generation
  • Reconsider default client generation without client decorators to minimize need for additional decoration
    • interface == operation group
    • interface == client
    • by default, operations outside an interface are excluded
  • Need clarity on whether there are subclients that cannot be instantiated, current understanding
    • Client == user must construct to use
    • SubClient == separately constructable, but also available through accessor on another client
    • OperationGroup == non-constructable group of operations, only instantiable through accessor
  • Perhaps separately provide client generation advice for resource manager clients

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment