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

Simple XML support #2970

Closed
allenjzhang opened this issue Feb 29, 2024 · 17 comments · Fixed by #3035
Closed

Simple XML support #2970

allenjzhang opened this issue Feb 29, 2024 · 17 comments · Fixed by #3035
Assignees
Labels
design:accepted Proposal for design has been discussed and accepted. triaged:core
Milestone

Comments

@allenjzhang
Copy link
Member

allenjzhang commented Feb 29, 2024

Xml Basic Support Design

To get basic support for xml we should get parity with OpenAPI

This means we need to have ways of specifying the following:

Field Name Type Description
name string Replaces the name of the element/attribute used for the described schema property. When defined within items, it will affect the name of the individual XML elements within the list. When defined alongside type being array (outside the items), it will affect the wrapping element and only if wrapped is true. If wrapped is false, it will be ignored.
namespace string The URI of the namespace definition. This MUST be in the form of an absolute URI.
prefix string The prefix to be used for the name.
attribute boolean Declares whether the property definition translates to an attribute instead of an element. Default value is false.
wrapped boolean MAY be used only for an array definition. Signifies whether the array is wrapped (for example, ) or unwrapped (). Default value is false. The definition takes effect only when defined alongside type being array (outside the items).
x-ms-text boolean Autorest doc | OpenAPI Issue

Attributes coverage

1. name

Name is something we can already provide with @encodedName("application/xml", "newname")

1.b Add @Xml.name decorator

Add an @Xml.name decorator that would just call @encodedName("application/xml", "newname") for you.
It should not be saving any extra state. It should really just be a shorthand for @encodedName("application/xml", "newname") and in no way using @xml.name("newname") achieve different functionality from @encodedName("application/xml", "newname").

2. attribute

Decorator would specify that this property should be serialized as an attribute instead.

extern dec attribute(target: ModelProperty);

Restrictions:

  • @attribute can only be applied to model properties of scalar types

3. wrapped and x-ms-text

Proposing that when dealing with arrays we always wrap them by default.
We are saying that there is always a node created for a property.
We then have a decorator @unwrapped that allows you to not wrap the property content.
In the case of an array property this is equivalent to setting wrapped: false in OpenAPI.
In the case of a scalar property this is equivalent to setting x-ms-text: true in OpenAPI with Autorest extensions.

extern dec unwrapped(target: ModelProperty);

4. namespace and prefix

Define a new namespace decorator that can be used in two ways.

extern dec ns(target: unknown, prefix: string, namespace: string)
extern dec ns(target: unknown, namespace: EnumMember)
  1. Simple but more verbose as you need to keep reusing the same namespace
@ns("ns1", "https://example.com/ns1")
model Foo {
  @ns("ns1", "https://example.com/ns1")
  bar: string
  @ns("ns2", "https://example.com/ns2")
  bar: string
}

You could also use an alias to reuse

alias ns1 = "https://example.com/ns1";
alias ns2 = "https://example.com/ns2";
@ns("ns1", ns1)
model Foo {
  @ns("ns1", ns1)
  bar: string
  @ns("ns2", ns2)
  bar: string
}
  1. Using an enum to define the namespace:
enum Namespaces {
  ns1 = "https://example.com/ns1",
  ns2 = "https://example.com/ns2"
}

@Xml.ns(Namespaces.ns1)
model Foo {
  @Xml.ns(Namespaces.ns1)
  bar: string
  @Xml.ns(Namespaces.ns2)
  bar: string
}

4.a Do we need a decorator to annoate the enum?

@Xml.nsDeclarations
enum Namespaces {
  ns1 = "https://example.com/ns1",
  ns2 = "https://example.com/ns2"
}

@Xml.ns(Namespaces.ns1)
model Foo {
  @Xml.ns(Namespaces.ns1)
  bar: string
  @Xml.ns(Namespaces.ns2)
  bar: string
}

Default encoding of scalars

As in Json we need to have some default handling of the common scalars like utcDateTime

Scalar Type Default Encoding Encoding name
utcDateTime xs:dateTime TypeSpec.Xml.Encoding.xmlDateTime
offsetDateTime xs:dateTime TypeSpec.Xml.Encoding.xmlDateTime
plainDate xs:date TypeSpec.Xml.Encoding.xmlDate
plainTime xs:time TypeSpec.Xml.Encoding.xmlTim
duration xs:duration TypeSpec.Xml.Encoding.xmlDuration
bytes xs:base64Binary TypeSpec.Xml.Encoding.xmlBase64Binary

Changes to add

Create new @typespec/xml library

New decorators

namespace TypeSpec.Xml
extern dec name(target: ModelProperty);
extern dec attribute(target: ModelProperty);
extern dec unwrapped(target: ModelProperty);
extern dec ns(target: unknown, namespaceOrPrefix: EnumMember | valueof string, namespace?: string);
extern dec nsDeclaration(target: Enum);

enum Encoding {
  /** Maps to encoding to a value used in xs:date */
  xmlDate,
  /** Maps to encoding to a value used in xs:time  */
  xmlTime,
  /** Maps to encoding to a value used in xs:dateTime  */
  xmlDateTime,
  /** Maps to encoding to a value used in xs:duration  */
  xmlDuration,
  /** Maps to encoding to a value used in xs:base64Binary  */
  xmlBase64Binary,
}

Apis to Add

To figure out if it is better to have those as fully qualifed id to the enum member name or just return the enum member directly.
This most likely need a change in the compiler to

  1. return the fully qualified enum member name(unless they are from the compiler)
  2. return the enum member directly via a different api ?
export function resolveXmlEncoding(type: Scalar): XmlEncoding | string;

export type XmlEncoding =
  | "TypeSpec.Xml.Encoding.xmlDate"
  | "TypeSpec.Xml.Encoding.xmlDate"
  | "TypeSpec.Xml.Encoding.xmlDateTime"
  | "TypeSpec.Xml.Encoding.xmlDuration"
  | "TypeSpec.Xml.Encoding.xmlBase64Binary"
  | string;

Examples

1. Array of primitive types

Scenario TypeSpec Xml OpenAPI3

Scenario 1.1:

  • ❌ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @xml.unwrapped
  tags: string[];
}
<XmlPet>
  <tags>abc</tags>
  <tags>def</tags>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      items:
        type: string
  xml:
    name: "XmlPet"

Scenario 1.2:

  • ❌ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  tags: string[];
}
<XmlPet>
  <ItemsTags>
    <string>abc</string>
    <string>def</string>
  </ItemsTags>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        type: string
  xml:
    name: "XmlPet"

Scenario 1.3:

  • ✅ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "ItemsName")
scalar tag extends string;

@encodedName("application/xml", "XmlPet")
model Pet {
  @xml.unwrapped
  tags: tag[];
}
<XmlPet>
  <ItemsName>abc</ItemsName>
  <ItemsName>def</ItemsName>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
      items:
        type: string
        xml:
          name: ItemsName
  xml:
    name: "XmlPet"

Scenario 1.4:

  • ✅ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "ItemsName")
scalar tag extends string;

@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  tags: tag[];
}
<XmlPet>
  <ItemsTags>
    <ItemsName>abc</ItemsName>
    <ItemsName>def</ItemsName>
  </ItemsTags>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        type: string
        xml:
          name: ItemsName
  xml:
    name: "XmlPet"

2. Complex array types

Scenario TypeSpec Xml OpenAPI3

Scenario 2.1:

  • ❌ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @xml.unwrapped
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <XmlTag>
    <name>string</name>
  </XmlTag>
</XmlPet>
Tag:
  type: "object"
  properties:
    name:
      type: "string"
  xml:
    name: "XmlTag"
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      items:
        $ref: "#/definitions/Tag"
  xml:
    name: "XmlPet"

Scenario 2.2:

  • ❌ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <ItemsTags>
    <XmlTag>
      <name>string</name>
    </XmlTag>
  </ItemsTags>
</XmlPet>
Tag:
  type: "object"
  properties:
    name:
      type: "string"
  xml:
    name: "XmlTag"
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        $ref: "#/definitions/Tag"
  xml:
    name: "XmlPet"

Scenario 2.3:

  • ✅ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  @xml.unwrapped
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <ItemsTag>
    <name>string</name>
  </ItemsTag>
</XmlPet>
Tag:
    type: "object"
    properties:
      name:
        type: "string"
    xml:
      name: "XmlTag"
  Pet:
    type: "object"
    properties:
      tags:
        type: "array"
        xml:
          name: "ItemsTags"
        items:
          $ref: "#/definitions/Tag"
          xml:
              name: ItemsXMLName
    xml:
      name: "XmlPet"

Scenario 2.4:

  • ✅ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <ItemsTags>
    <XmlTag>
      <name>string</name>
    </XmlTag>
  </ItemsTags>
</XmlPet>
Tag:
  type: "object"
  properties:
    name:
      type: "string"
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        $ref: "#/definitions/Tag"
  xml:
    name: "XmlPet"

3. Nested models

Scenario TypeSpec Xml OpenAPI3

Scenario 3.1:

No annotations

model Book {
  author: Author;
}

model Author {
  name: string;
}
<Book>
  <author>
    <name>string</name>
  </author>
</Book>
Book:
  type: object
  properties:
    author:
      $ref: "#/components/schemas/Author"
Author:
  type: object
  properties:
    name:
      type: string

Scenario 3.2:

Nested model has xml encoded name.

⚠️ no op in serialization of Book

model Book {
  author: Author;
}

@encodedName("application/xml", "XmlAuthor")
model Author {
  name: string;
}
<Book>
  <author>
    <name>string</name>
  </author>
</Book>
Book:
  type: object
  properties:
    author:
      allOf:
        - $ref: "#/components/schemas/Author"
      xml:
        name: "author" # Here we have to redefine this name otherwise in OpenAPI semantic the `XmlAuthor` name would be used
Author:
  type: object
  properties:
    name:
      type: string
  xml:
    name: "XmlAuthor"

Scenario 3.2:

Property has encoded name

model Book {
  @encodedName("application/xml", "xml-author")
  author: Author;
}

model Author {
  name: string;
}
<Book>
  <xml-author>
    <name>string</name>
  </xml-author>
</Book>
Book:
  type: object
  properties:
    author:
      allOf:
        - $ref: "#/components/schemas/Author"
      xml:
        name: "xml-author"
Author:
  type: object
  properties:
    name:
      type: string

4. Attributes

Scenario TypeSpec Xml OpenAPI3

Scenario 4.1:

Convert a property to an attribute

model Book {
  @Xml.attribute
  id: string;

  title: string;
  author: string;
}
<Book id="0">
  <xml-title>string</xml-title>
  <author>string</author>
</Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
      xml:
        name: "xml-title"
    author:
      type: string

5. Namespace and prefix (inline form)

Scenario TypeSpec Xml OpenAPI3

Scenario 5.1:

On model

@Xml.ns("smp", "http://example.com/schema")
model Book {
  id: string;
  title: string;
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema">
  <id>0</id>
  <title>string</title>
  <author>string</author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
    author:
      type: string
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

Scenario 5.2:

On model and properties

@Xml.ns("smp", "http://example.com/schema")
model Book {
  id: string;
  @Xml.ns("smp", "http://example.com/schema")
  title: string;
  @Xml.ns("ns2", "http://example.com/ns2")
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema" xmlns:sn2="http://example.com/ns2">
  <id>0</id>
  <smp:title>string</smp:title>
  <ns2:author>string</ns2:author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
       xml:
        prefix: "smp"
        namespace: "http://example.com/schema"
    author:
      type: string
      xml:
        prefix: "ns2"
        namespace: "http://example.com/ns2"
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

6. Namespace and prefix (normalized form)

Scenario TypeSpec Xml OpenAPI3

Scenario 6.1:

On model

@Xml.nsDeclarations
enum Namespaces {
  smp = "http://example.com/schema"
}

@Xml.ns(Namespaces.smp)
model Book {
  id: string;
  title: string;
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema">
  <id>0</id>
  <title>string</title>
  <author>string</author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
    author:
      type: string
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

Scenario 6.2:

On model and properties

@Xml.nsDeclarations
enum Namespaces {
  smp = "http://example.com/schema",
  ns2 = "http://example.com/ns2"
}

@Xml.ns(Namespaces.smp)
model Book {
  id: string;
  @Xml.ns(Namespaces.smp)
  title: string;
  @Xml.ns(Namespaces.ns2)
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema" xmlns:sn2="http://example.com/ns2">
  <id>0</id>
  <smp:title>string</smp:title>
  <ns2:author>string</ns2:author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
       xml:
        prefix: "smp"
        namespace: "http://example.com/schema"
    author:
      type: string
      xml:
        prefix: "ns2"
        namespace: "http://example.com/ns2"
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

6. Property setting the text of the node

Scenario TypeSpec Xml OpenAPI3

Scenario 6.1:

model BlobName {
  @Xml.attribute language: string;
  @Xml.unwrapped content: string;
}
<BlobName language="abc">
  ...content...
</smp:Book>
Book:
  type: object
  properties:
    language:
      type: string
    content:
      type: string
      xml:
        x-ms-text: true # on autorest emitter
@timotheeguerin
Copy link
Member

I think this should be in a separeate@typespec/xml library that describe xml serialization

@timotheeguerin
Copy link
Member

also name should probably just be the value from @encodedName("application/xml", ...)

@bterlson
Copy link
Member

Agree we probably want @typespec/xml. I know that the proposal says "The scope of this proposal only covers the design and implementation of Xml support in OpenAPI, not any general Xml/XSD/WSDL support", but I think this is not what TypeSpec is about. We should look at this as general XML support, such that we can today emit everything we need into OpenAPI and drive codegen for clients consuming XML. We do not need to implement an XSD emitter but we should be able to convince ourselves that we can write that should the need arise.

@allenjzhang
Copy link
Member Author

allenjzhang commented Mar 1, 2024

This is to cover the enhancement/gaps to the full compliance to OAS standards. I don't think this poposal conflict or overlap with a full @typespec/xml work item.

That is another full scope of work entirely:

  1. Do we have asks for @typespec/xml? Even if we have generic @typespec/xml, we will need to use similar ways to apply to OAS ( ie @xmlObject) I don't think we are doing OAS spec writer any favor to ask they put bunch of generic Xml decorators to describe @xmlObject and then with lint rules to ensure few applies here.
  2. We need to properly define the full XML support which would take more time.

also name should probably just be the value from @Encodedname("application/xml", ...)
While that is true, it is not intuitive given there are other metadata that need to be applied.

Again, this is to round up feature gaps for OAS, and lives under OpenAPI or OpenAPI.Xml.

@timotheeguerin
Copy link
Member

timotheeguerin commented Mar 1, 2024

If the goal is to generate storage from typespec directly we need @typespec/xml we cannot have the rust emitter depend on the openapi decorator.

do we know all the xml attributes that storage is using? if its just attribute:true and name could we just do that for now in this new package?

Could we also maybe then mark it as experimental(like projection log a warning) if we are unsure of where it is going?

@allenjzhang
Copy link
Member Author

allenjzhang commented Mar 1, 2024

If the goal is to generate storage from typespec directly we need @typespec/xml we cannot have the rust emitter depend on the openapi decorator.

Does any current emitters take dependency on openapi? How does emitters do today to access any of the extension decorators (x-ms)? Does TCGC proxy it thru for language emitters?

But this a good point.

do we know all the xml attributes that storage is using? if its just attribute:true and name could we just do that for now in this new package?
I do see name, attribute, wrapped used.

@timotheeguerin
Copy link
Member

So we could imaging the following as a first step

name -> `@encodedName("application/xml", "newName")`
wrapped  -> `@Xml.attribute`
wrapped -> `@Xml.wrapped`

The prefix and namespace is what is much more confusing and we might have multiple ways of doing it.

@allenjzhang
Copy link
Member Author

allenjzhang commented Mar 1, 2024

I am thinking to pivot to a new library @typespec/xml with namespace of TypeSpec.Xml.OpenAPI and place the above proposal there. We can figure out what goes under TypeSpec.Xml later. This would also give OAS emitters and spec writers a good interop with OAS.

@bterlson
Copy link
Member

bterlson commented Mar 2, 2024

I don't understand why we need that, yet. Do you think we can't achieve good interop with general purpose Xml annotations?

@markcowl markcowl added this to the [2024] April milestone Mar 4, 2024
@markcowl markcowl added design:needed A design request has been raised that needs a proposal triaged:core and removed needs-area labels Mar 4, 2024
@timotheeguerin
Copy link
Member

Also have this dup issue if #1180

@timotheeguerin
Copy link
Member

timotheeguerin commented Mar 4, 2024

Xml Basic Support Proposal

To get basic support for xml we should get parity with OpenAPI

This means we need to have ways of specifying the following:

Field Name Type Description
name string Replaces the name of the element/attribute used for the described schema property. When defined within items, it will affect the name of the individual XML elements within the list. When defined alongside type being array (outside the items), it will affect the wrapping element and only if wrapped is true. If wrapped is false, it will be ignored.
namespace string The URI of the namespace definition. This MUST be in the form of an absolute URI.
prefix string The prefix to be used for the name.
attribute boolean Declares whether the property definition translates to an attribute instead of an element. Default value is false.
wrapped boolean MAY be used only for an array definition. Signifies whether the array is wrapped (for example, ) or unwrapped (). Default value is false. The definition takes effect only when defined alongside type being array (outside the items).

Attributes coverage

1. name

Name is something we can already provide with @encodedName("application/xml", "<newname>")

1.b Consider @Xml.name decorator

If overriding the name is common enough we could consider adding a @Xml.name decorator that would just call @encodedName("application/xml", "<newname>") for you.

2. attribute

Decorator would specify that this property should be serialized as an attribute instead.

extern dec attribute(target: ModelProperty);

3. wrapped

Proposing that when dealing with arrays we always wrap them by default. And instead we have a flattenArray decorator to remove the wrapping.

extern dec flattenArray(target: ModelProperty);

4. namespace and prefix

Define a new namespace decorator that can be used in two ways.

extern dec namespace(target: unknown, prefix: string, namespace: string)
extern dec namespace(target: unknown, namespace: EnumMember)
  1. Simple but more verbose as you need to keep reusing the same namespace
@namespace("ns1", "https://example.com/ns1")
model Foo {
  @namespace("ns1", "https://example.com/ns1")
  bar: string
  @namespace("ns2", "https://example.com/ns2")
  bar: string
}

You could also use an alias to reuse

alias ns1 = "https://example.com/ns1";
alias ns2 = "https://example.com/ns2";
@namespace("ns1", ns1)
model Foo {
  @namespace("ns1", ns1)
  bar: string
  @namespace("ns2", ns2)
  bar: string
}
  1. Using an enum to define the namespace:
enum Namespaces {
  ns1 = "https://example.com/ns1",
  ns2 = "https://example.com/ns2"
}

@Xml.namespace(Namespaces.ns1)
model Foo {
  @Xml.namespace(Namespaces.ns1)
  bar: string
  @Xml.namespace(Namespaces.ns2)
  bar: string
}

4.a Do we need a decorator to annoate the enum?

@Xml.namespaceDeclarations
enum Namespaces {
  ns1 = "https://example.com/ns1",
  ns2 = "https://example.com/ns2"
}

@Xml.namespace(Namespaces.ns1)
model Foo {
  @Xml.namespace(Namespaces.ns1)
  bar: string
  @Xml.namespace(Namespaces.ns2)
  bar: string
}

Shorter names

  • @encodedName -> @Xml.name
  • @Xml.attribute -> @Xml.attr
  • @Xml.namespace -> @Xml.ns
  • @Xml.namespaceDeclarations -> @Xml.nsDeclarations
  • @Xml.flattenArray -> @Xml.flatten

Examples

1. Array of primitive types

Scenario TypeSpec Xml OpenAPI3

Scenario 1.1:

  • ❌ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @Xml.flattenArray
  tags: string[];
}
<XmlPet>
  <tags>string</tags>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      items:
        type: string
  xml:
    name: "XmlPet"

Scenario 1.2:

  • ❌ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  tags: string[];
}
<XmlPet>
  <ItemsTags>
    <ItemsTags>string</ItemsTags>
  </ItemsTags>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        type: string
  xml:
    name: "XmlPet"

Scenario 1.3:

  • ✅ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "ItemsName")
scalar tag extends string;

@encodedName("application/xml", "XmlPet")
model Pet {
  @Xml.flattenArray
  tags: tag[];
}
<XmlPet>
  <ItemsName>string</ItemsName>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
      items:
        type: string
        xml:
          name: ItemsName
  xml:
    name: "XmlPet"

Scenario 1.4:

  • ✅ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "ItemsName")
scalar tag extends string;

@encodedName("application/xml", "XmlPet")
model Pet {
  tags: tag[];
}
<XmlPet>
  <ItemsTags>
    <ItemsName>string</ItemsName>
  </ItemsTags>
</XmlPet>
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        type: string
        xml:
          name: ItemsName
  xml:
    name: "XmlPet"

2. Complex array types

Scenario TypeSpec Xml OpenAPI3

Scenario 2.1:

  • ❌ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @Xml.flattenArray
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <XmlTag>
    <name>string</name>
  </XmlTag>
</XmlPet>
Tag:
  type: "object"
  properties:
    name:
      type: "string"
  xml:
    name: "XmlTag"
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      items:
        $ref: "#/definitions/Tag"
  xml:
    name: "XmlPet"

Scenario 2.2:

  • ❌ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <ItemsTags>
    <XmlTag>
      <name>string</name>
    </XmlTag>
  </ItemsTags>
</XmlPet>
Tag:
  type: "object"
  properties:
    name:
      type: "string"
  xml:
    name: "XmlTag"
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        $ref: "#/definitions/Tag"
  xml:
    name: "XmlPet"

Scenario 2.3:

  • ✅ ItemsName
  • ❌ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  @Xml.flattenArray
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <ItemsTag>
    <name>string</name>
  </ItemsTag>
</XmlPet>
Tag:
    type: "object"
    properties:
      name:
        type: "string"
    xml:
      name: "XmlTag"
  Pet:
    type: "object"
    properties:
      tags:
        type: "array"
        xml:
          name: "ItemsTags"
        items:
          $ref: "#/definitions/Tag"
          xml:
              name: ItemsXMLName
    xml:
      name: "XmlPet"

Scenario 2.4:

  • ✅ ItemsName
  • ✅ Wrapped
@encodedName("application/xml", "XmlPet")
model Pet {
  @encodedName("application/xml", "ItemsTags")
  tags: Tag[];
}

@encodedName("application/xml", "XmlTag")
model Tag {
  name: string;
}
<XmlPet>
  <ItemsTags>
    <XmlTag>
      <name>string</name>
    </XmlTag>
  </ItemsTags>
</XmlPet>
Tag:
  type: "object"
  properties:
    name:
      type: "string"
Pet:
  type: "object"
  properties:
    tags:
      type: "array"
      xml:
        name: "ItemsTags"
        wrapped: true
      items:
        $ref: "#/definitions/Tag"
  xml:
    name: "XmlPet"

3. Nested models

Scenario TypeSpec Xml OpenAPI3

Scenario 3.1:

No annotations

model Book {
  author: Author;
}

model Author {
  name: string;
}
<Book>
  <author>
    <name>string</name>
  </author>
</Book>
Book:
  type: object
  properties:
    author:
      $ref: "#/components/schemas/Author"
Author:
  type: object
  properties:
    name:
      type: string

Scenario 3.2:

Nested model has xml encoded name.

⚠️ no op in serialization of Book

model Book {
  author: Author;
}

@encodedName("application/xml", "XmlAuthor")
model Author {
  name: string;
}
<Book>
  <author>
    <name>string</name>
  </author>
</Book>
Book:
  type: object
  properties:
    author:
      allOf:
        - $ref: "#/components/schemas/Author"
      xml:
        name: "author" # Here we have to redefine this name otherwise in OpenAPI semantic the `XmlAuthor` name would be used
Author:
  type: object
  properties:
    name:
      type: string
  xml:
    name: "XmlAuthor"

Scenario 3.2:

Property has encoded name

model Book {
  @encodedName("application/xml", "xml-author")
  author: Author;
}

model Author {
  name: string;
}
<Book>
  <xml-author>
    <name>string</name>
  </xml-author>
</Book>
Book:
  type: object
  properties:
    author:
      allOf:
        - $ref: "#/components/schemas/Author"
      xml:
        name: "xml-author"
Author:
  type: object
  properties:
    name:
      type: string

4. Attributes

Scenario TypeSpec Xml OpenAPI3

Scenario 4.1:

Convert a property to an attribute

model Book {
  @Xml.attribute
  id: string;

  title: string;
  author: string;
}
<Book>
  <id>0</id>
  <xml-title>string</xml-title>
  <author>string</author>
</Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
      xml:
        name: "xml-title"
    author:
      type: string

5. Namespace and prefix (inline form)

Scenario TypeSpec Xml OpenAPI3

Scenario 5.1:

On model

@Xml.namespace("smp", "http://example.com/schema")
model Book {
  id: string;
  title: string;
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema">
  <id>0</id>
  <title>string</title>
  <author>string</author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
    author:
      type: string
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

Scenario 5.2:

On model and properties

@Xml.namespace("smp", "http://example.com/schema")
model Book {
  id: string;
  @Xml.namespace("smp", "http://example.com/schema")
  title: string;
  @Xml.namespace("ns2", "http://example.com/ns2")
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema" xmlns:sn2="http://example.com/ns2">
  <id>0</id>
  <smp:title>string</smp:title>
  <ns2:author>string</ns2:author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
       xml:
        prefix: "smp"
        namespace: "http://example.com/schema"
    author:
      type: string
      xml:
        prefix: "ns2"
        namespace: "http://example.com/ns2"
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

6. Namespace and prefix (normalized form)

Scenario TypeSpec Xml OpenAPI3

Scenario 6.1:

On model

@Xml.namespaceDeclarations
enum Namespaces {
  smp = "http://example.com/schema"
}

@Xml.namespace(Namespaces.smp)
model Book {
  id: string;
  title: string;
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema">
  <id>0</id>
  <title>string</title>
  <author>string</author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
    author:
      type: string
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

Scenario 6.2:

On model and properties

@Xml.namespaceDeclarations
enum Namespaces {
  smp = "http://example.com/schema",
  ns2 = "http://example.com/ns2"
}

@Xml.namespace(Namespaces.smp)
model Book {
  id: string;
  @Xml.namespace(Namespaces.smp)
  title: string;
  @Xml.namespace(Namespaces.ns2)
  author: string;
}
<smp:Book xmlns:smp="http://example.com/schema" xmlns:sn2="http://example.com/ns2">
  <id>0</id>
  <smp:title>string</smp:title>
  <ns2:author>string</ns2:author>
</smp:Book>
Book:
  type: object
  properties:
    id:
      type: integer
    title:
      type: string
       xml:
        prefix: "smp"
        namespace: "http://example.com/schema"
    author:
      type: string
      xml:
        prefix: "ns2"
        namespace: "http://example.com/ns2"
  xml:
    prefix: "smp"
    namespace: "http://example.com/schema"

@timotheeguerin
Copy link
Member

@allenjzhang
Copy link
Member Author

allenjzhang commented Mar 4, 2024

Thanks for updating to TypeSpec.Xml namespace.

  1. I like the convenience of 1.b but the dev doc should clearly indicate it is aliased from @encodedName
  2. @Xml.attribute sounds fine.
  3. I think wrapped is a common term used in Xml Array and very intuitive as well. flattenArray doesn't quite capture the essence and may lead to misunderstanding. Maybe unwrappedArray?
  4. @namespace looks good.

An uber comment, I prefer a prefix xml to the decorators. I know it looks redundant when you reference the namespace, @Xml.xmlAttribute, but in the cases using the namespace at the top, the code is much more readable. @xmlAttribute, @xmlNamespace.

@timotheeguerin
Copy link
Member

  1. I agree that wrapped sounds quite intuitive but to have wrapped be the default which I think makes the most sense, i am not sure having unwrappedArray looks clean. Smithy use @flattened, CSharp use [Element]. Could do just unwrapped

For the prefix I do agree that if you use the using it becomes confusing but I think this is a case of a library you wouldn't want to be using the using ever. I don't really like having an extra prefix just because people might be doing something. they don't need to and can keep the cleaar form with @Xml.xyz

@bterlson
Copy link
Member

bterlson commented Mar 4, 2024

I also don't support the built-in prefix. With the current design you can use the xml prefix (with or without using) or not use the xml prefix (with using), depending on personal preference. Seems best to me.

@timotheeguerin timotheeguerin self-assigned this Mar 4, 2024
This was referenced Mar 4, 2024
@timotheeguerin timotheeguerin changed the title Feature request: Adding XmlObject support for OpenAPI library and emitters Simple XML support Mar 6, 2024
@timotheeguerin timotheeguerin changed the title Simple XML support Design: Simple XML support Mar 6, 2024
@timotheeguerin
Copy link
Member

Decision from design meeting

  • @attribute limit to scalars
  • decide default encoding of scalars for xml
  • Add @Xml.name which calls @encodedName

@timotheeguerin timotheeguerin added design:proposed Proposal has been added and ready for discussion and removed design:needed A design request has been raised that needs a proposal design:proposed Proposal has been added and ready for discussion labels Mar 6, 2024
@timotheeguerin timotheeguerin added the design:accepted Proposal for design has been discussed and accepted. label Mar 7, 2024
@timotheeguerin timotheeguerin changed the title Design: Simple XML support Simple XML support Mar 7, 2024
@allenjzhang
Copy link
Member Author

#3010

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
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