Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples #2700

Closed
9 tasks
markcowl opened this issue Nov 27, 2023 · 4 comments · Fixed by #3572
Closed
9 tasks

Examples #2700

markcowl opened this issue Nov 27, 2023 · 4 comments · Fixed by #3572
Assignees
Labels
compiler:core Issues for @typespec/compiler design:accepted Proposal for design has been discussed and accepted. triaged:core
Milestone

Comments

@markcowl
Copy link
Contributor

markcowl commented Nov 27, 2023

Implementation issue for: https://github.com/Azure/typespec-azure/issues/3540, #514. Design details are here

  • Update versioning decorators to accommodate 'never' for the version argument (meaning do not execute the decorator)

  • Add templates and decorators to OpenAPI library

    • Add JsonExample templates, and, in private namespace, @jsonExampleName, @jsonExampleLiteral , @jsonExampleParameters, @jsonExampleReturnValue decorators and associated accessors:
      @jsonExampleName(TName)
      @added(TVersionAdded)
      @removed(TVersionRemoved)
      model JsonExampleOptions<
        TName extends string = null,
        TVersionAdded extends EnumMember = never,
        TVersionRemoved extends EnumMember = never> {}
    
      @jsonExampleLiteral(TExample)
      model JsonExample<
        TExample extends unknown,
        TName extends string = null,
        TSummary extends string = null,
        TVersionAdded extends EnumMember = never,
        TVersionRemoved extends EnumMember = never
      > is ExampleOptions<TName, TVersionAdded, TVersionRemoved> {}
    
      // This is specifically for matching return values in OpenAPI, with a
      // model keyed by status code
      @jsonExampleParameters(TParams)
      @jsonExampleReturnValue(TReturnValue)
      model JsonOperationExample<
        TParams extends {},
        TReturnValue extends Model | Model[],
        TName extends string = null,
        TSummary extends string = null,
        TVersionAdded extends EnumMember = never,
        TVersionRemoved extends EnumMember = never
      > is ExampleOptions<TName, TVersionAdded, TVersionRemoved> {}
    
    • Add @jsonExample decorator and associated accessors
    extern dec jsonExample(target: Model | ModelProperty , example: unknown)
    extern dec jsonOperationExample(target: Operation , example: unknown)
    
    • Add JsonExternalExample templates and associated decorator (@jsonExamplePath and accessor in private namespace)
    @jsonExamplePath(TExternalValue)
    model JsonExternalExample<
      TExternalValue extends string,
      TName extends string,
      TSummary extends string = null,
      TVersionAdded extends EnumMember = never,
      TVersionRemoved extends EnumMember = never
    > is ExampleOptions<TName, TVersionAdded, TVersionRemoved> {}
    
    • Add @jsonExternalExample decorator and associated accessors
    extern dec jsonExternalExample(target: Model | ModelProperty | Operation, externalExample: unknown)
    
  • Implement OpenAPI3 example support based on decorators

    • Model and ModelProperty examples: support inlined value examples and named examples:

    TypeSpec

    @jsonExample(
      JsonExample<
        {type: "bar", createdAt: "2023-03-01T19:00:00Z"}, 
        TName = "bar-example", 
        TSummary ="An example of a bar type foo">)
    model Foo {
      type: "bar" | "baz"
      @jsonExample("2023-03-01T19:00:00Z")
      createdAt: utcDateTime;
    }
    

    Corresponding OpenAPI3

      Foo:
       type: object
       examples:
         bar-example:
           summary: "An example of a bar type foo"
           value:
             type: "bar"
             createdAt: "2023-03-01T19:00:00Z"
       properties:
         createdAt: 
           type: string
           format: date-time
           examples:
             - "2023-03-01T19:00:00Z"
    • Example file support in OpenAPI3

    TypeSpec

    @jsonExternalExample(
      JsonExternalExample<
        "examples/simple-foo-example.json", 
        TName = "simple-foo-example", 
        TSummary ="A simple example of type foo">)
    model Foo {
      type: "bar" | "baz"
      createdAt: utcDateTime;
    }
    

    Corresponding OpenAPI:

      Foo:
         type: object
         examples:
           simple-foo-example:
             summary: A simple example of type foo
             externalValue: 'examples/simple-foo-example.json'
         properties:
           createdAt: 
             type: string
             format: date-time
@markcowl
Copy link
Contributor Author

markcowl commented Dec 4, 2023

est: 13

@timotheeguerin
Copy link
Member

timotheeguerin commented May 1, 2024

Examples with the new value world

With the introduction of the new values in TypeSpec, examples are a great fit for making use of those.
One of the problem with the previous approach was it was limtied to Json examples as we didn't know how to represent non primitive scalars(a canonical datetime for example) but this is all things that are able to be represented with the new scalar constructors.

Types examples

First and easier part is to define examples for the types. This would give parity with OpenAPI3

Decorator

model ExampleOptions {
  /** The title of the example */
  title?: string;
  /** Description of the example */
  description?: string;
  /** Scope this example can apply to */
  scopes?: unknown[];
}
extern dec example(target, value: valueof unknown, options: valueof ExampleOptions);

Example scopes

We could say that the title and description are good enough to be able to distinguish across version and visibility as well as some automatic filtering of non applicable properties could work.
However there is most likely times where you want to be more explicit.

Proposing each examples can have a list of applicable scopes. It is then the job of the emitter to filter out examples that are not applicable to their scope.

Potential scopes:

  • Serialization format: forMediaType.is("application/json")
  • Specific version: forVersion.is(Versions.v3)
  • Specific visibility: forVisibility.is("read")
  • Specific emitter: Typespec.Http.Client.TypeScript.forEmitter()

With this it means that the value parameter doesn't necessary needs to be a value of the target type. It could be the json serialized payload or some CSharp or TypeScript example code.

Separate decorators for protocol/different targets

dec TypeSpec.Json.example(target, value: valueof unknown, options: ExampleOptions );
dec Typespec.Http.Client.TypeScript.example(target, value: valueof string, options: ExampleOptions );
dec Typespec.Http.Client.Csharp.example(target, value: valueof string, options: ExampleOptions );

Examples

Simple model

@example(#{
  name: "John",
  age: 12,
  birthDate: plainDate.fromISO("2000-01-01")
})
model Person {
  name: string;
  age: int32;
  birthDate: plainDate;
}

Simple property

Same as above but the examples are on the properties

model Person {
  @example("John")
  name: string;
  @example(24)
  age: int32;
  @example(plainDate.fromISO("2000-01-01"))
  birthDate: plainDate;
}

Visibility example

@example(#{
  id: "user-123",
  name: "John",
  birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[forVisibility.is("read")]})
@example(#{
  name: "John",
  password: "password123!",
  birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[forVisibility.is("create")]})
model Person {
  @visibility("read") id: string;
  @visibility("create") password: string;
  name: string;
  birthDate: plainDate;
}

Visibility example

model Person {
  @added(Version.v2) id?: string;
  name: string;
  birthDate: plainDate;
}

// Can be in a separate file
@@example(Person, #{
  id: "user-123",
  name: "John",
  birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[fromVersioning.is(Versions.v2)]})
@@example(Person, #{
  name: "John",
  birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[fromVersioning.range(Versions.v1, Version.v2)]})

Json inline

@Json.example(#{
  name: "John",
  age: 12,
  birthDate: "2000-01-01"
})
model Person {
  name: string;
  age: int32;
  birthDate: plainDate;
}

or

@Json.example("""
{
  "name": "John",
  "age": 12,
  "birthDate": "2000-01-01"
}
""")
model Person {
  name: string;
  age: int32;
  birthDate: plainDate;
}

External file

Load an external file. In this case we need to specify that this example is for a specific protocol

scalar externalExample {
  init path(path: string);
}

@Json.example(externalExample.path("person.json"))
model Person {
  name: string;
  age: int32;
  birthDate: plainDate;
}

Client Examples

Load an external file. In this case we need to specify that this example is for a specific protocol

@Http.Client.TypeScript.example("""
const person: Person = {
  name: "John",
  age: 12,
  birthDate: "2000-01-01"
};
""")
@Http.Client.CSharp.example("""
var person = new Person{
  name = "John",
  age = 12,
  birthDate = new Date("2000-01-01")
};
""")
model Person {
  name: string;
  age: int32;
  birthDate: plainDate;
}

Operation examples

Operation examples can be a little more tricky because depending on the protocol you might want to see things differently. But maybe its just the job of the emitter to move things in the same place that the model was moved(e.g. body)

Decorator

We have 2 options for decorators

  1. Use a dedicated decorator @opExample or @operationExample
  2. Use the same decorator as for the models just expect a different structure
model ExampleOptions {
  /** The title of the example */
  title?: string;
  /** Description of the example */
  description?: string;
  /** Scope this example can apply to */
  scopes?: unknown[];
}

model OperationExample {
  parameters?: unknown;
  returns?: unknown;
}
extern dec opExample(target, value: valueof OperationExample, options: valueof ExampleOptions);

Example scopes

Scopes would apply as well however some might be irrelevant as the operation would already for example define the visibility.

Examples

Operation with spread parameters

@opExample(#{
  parameters: #{
    name: "John",
    age: 12,
    birthDate: plainDate.fromISO("2000-01-01")
  },
  returns: #{
    name: "John",
    age: 12,
    birthDate: plainDate.fromISO("2000-01-01")
  }
})
op createUser(...User): User;

Operation with explicit body

@opExample(#{
  parameters: #{
    user: #{
      name: "John",
      age: 12,
      birthDate: plainDate.fromISO("2000-01-01")
    }
  },
  returns: #{
    name: "John",
    age: 12,
    birthDate: plainDate.fromISO("2000-01-01")
  }
})
op createUser(@body user: User): User;

Operation with spread body and headers

@opExample(#{
  parameters: #{
    name: "John",
    age: 12,
    birthDate: plainDate.fromISO("2000-01-01"),
    ifNotModified: eTag("abcdefghijklmnop")
  },
})
op updateUser(...User, @header ifNotModified: eTag): void;

Operation with explicit body and headers

@opExample(#{
  parameters: #{
    user: #{
      name: "John",
      age: 12,
      birthDate: plainDate.fromISO("2000-01-01")
    },
    ifNotModified: eTag("abcdefghijklmnop")
  },
})
op updateUser(@body user: User, @header ifNotModified: eTag): void;

External canonical examples

Above we showed that we could have the example be loaded from an external file but that would be specific to a protocol(e.g. a json file, an xml file, some csharp code)

We could imaging having a special file extension that define canonical examples with TypeSpec. This is just a concept there is a lot more to think about this

  • examples/person.example.tsp
import "../scalars.tsp";

example "Example title" #{
  name: "John",
  age: 12,
  birthDate: plainDate.fromISO("2000-01-01"),
  data: customScalar.from("some data")
}
  • scalar.tsp
scalar customScalar {
  from(value: string);
}
  • models.tsp
@example(import("./examples/person.example.tsp"))
model Person {
  name: string;
  age: int32;
  birthDate: plainDate;
  data: customScalar;
}

Stage 1:

  • TypeSpec only examples
  • Visibility and versioning are automatically applied to examples
  • Provide view of the examples for certain protocol(Json, xml library can provide its own implementation)
  • different library? @typespec/experimental-examples
    • emit a single warning on import?
interface Example {
  type: Type;
  value: Value;
}
function getExample(): Example;
function toJsonExample(example: Example): unknown;
function toXmlExample(example: Example): unknown;
// http lib
function filterHttpVisibility(
  example: Example,
  visibility: Visibility
): Example;
// Versioning lib
function filterVersioning(example: Example, version: Version): Example;

@timotheeguerin timotheeguerin changed the title Implementation: Add support for versioned examples Examples May 1, 2024
@markcowl markcowl modified the milestones: [2024] May, [2024] June May 6, 2024
@ArcturusZhang
Copy link
Member

I have a question, as for the opExample decorator, would we have validations/static checks for the types?
Such as this example:

@opExample(#{
  parameters: #{
    user: #{
      name: "John",
      age: 12,
      birthDate: plainDate.fromISO("2000-01-01")
    },
    ifNotModified: eTag("abcdefghijklmnop")
  },
})
op updateUser(@body user: User, @header ifNotModified: eTag): void;

what if the json we have in the example decorator has some missing required property? Would compile report error?

@tadelesh
Copy link
Member

the opExample is on typespec operation and example value should be one one mapping to operation's parameters and return types, right? since typespec has template, it seems it is not easy to know all the parameters and responses structure, thus not easy to write an example.

@timotheeguerin timotheeguerin added design:accepted Proposal for design has been discussed and accepted. compiler:core Issues for @typespec/compiler and removed design:needed A design request has been raised that needs a proposal labels Jul 9, 2024
@markcowl markcowl added this to the [2024] July milestone Jul 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:core Issues for @typespec/compiler design:accepted Proposal for design has been discussed and accepted. triaged:core
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants