diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index 90d5c545b..8ed048427 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -60,6 +60,66 @@ public static class OpenApiConstants /// </summary> public const string Format = "format"; + /// <summary> + /// Field: Schema + /// </summary> + public const string DollarSchema = "$schema"; + + /// <summary> + /// Field: Id + /// </summary> + public const string Id = "$id"; + + /// <summary> + /// Field: Comment + /// </summary> + public const string Comment = "$comment"; + + /// <summary> + /// Field: Vocabulary + /// </summary> + public const string Vocabulary = "$vocabulary"; + + /// <summary> + /// Field: DynamicRef + /// </summary> + public const string DynamicRef = "$dynamicRef"; + + /// <summary> + /// Field: DynamicAnchor + /// </summary> + public const string DynamicAnchor = "$dynamicAnchor"; + + /// <summary> + /// Field: RecursiveRef + /// </summary> + public const string RecursiveRef = "$recursiveRef"; + + /// <summary> + /// Field: RecursiveAnchor + /// </summary> + public const string RecursiveAnchor = "$recursiveAnchor"; + + /// <summary> + /// Field: Definitions + /// </summary> + public const string Defs = "$defs"; + + /// <summary> + /// Field: V31ExclusiveMaximum + /// </summary> + public const string V31ExclusiveMaximum = "exclusiveMaximum"; + + /// <summary> + /// Field: V31ExclusiveMinimum + /// </summary> + public const string V31ExclusiveMinimum = "exclusiveMinimum"; + + /// <summary> + /// Field: UnevaluatedProperties + /// </summary> + public const string UnevaluatedProperties = "unevaluatedProperties"; + /// <summary> /// Field: Version /// </summary> diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs new file mode 100644 index 000000000..66fa00acd --- /dev/null +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -0,0 +1,795 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; + +namespace Microsoft.OpenApi.Models +{ + /// <summary> + /// The Schema Object allows the definition of input and output data types. + /// </summary> + public class OpenApiSchema : IOpenApiExtensible, IOpenApiReferenceable, IOpenApiSerializable + { + /// <summary> + /// Follow JSON Schema definition. Short text providing information about the data. + /// </summary> + public string Title { get; set; } + + /// <summary> + /// $schema, a JSON Schema dialect identifier. Value must be a URI + /// </summary> + public string Schema { get; set; } + + /// <summary> + /// $id - Identifies a schema resource with its canonical URI. + /// </summary> + public string Id { get; set; } + + /// <summary> + /// $comment - reserves a location for comments from schema authors to readers or maintainers of the schema. + /// </summary> + public string Comment { get; set; } + + /// <summary> + /// $vocabulary- used in meta-schemas to identify the vocabularies available for use in schemas described by that meta-schema. + /// </summary> + public string Vocabulary { get; set; } + + /// <summary> + /// $dynamicRef - an applicator that allows for deferring the full resolution until runtime, at which point it is resolved each time it is encountered while evaluating an instance + /// </summary> + public string DynamicRef { get; set; } + + /// <summary> + /// $dynamicAnchor - used to create plain name fragments that are not tied to any particular structural location for referencing purposes, which are taken into consideration for dynamic referencing. + /// </summary> + public string DynamicAnchor { get; set; } + + /// <summary> + /// $recursiveAnchor - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#") + /// </summary> + public string RecursiveAnchor { get; set; } + + /// <summary> + /// $recursiveRef - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#") + /// </summary> + public string RecursiveRef { get; set; } + + /// <summary> + /// $defs - reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema. + /// The keyword does not directly affect the validation result + /// </summary> + public IDictionary<string, OpenApiSchema> Definitions { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public decimal? V31ExclusiveMaximum { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public decimal? V31ExclusiveMinimum { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public bool UnEvaluatedProperties { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Value MUST be a string in V2 and V3. + /// </summary> + public object Type { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// While relying on JSON Schema's defined formats, + /// the OAS offers a few additional predefined formats. + /// </summary> + public string Format { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// CommonMark syntax MAY be used for rich text representation. + /// </summary> + public string Description { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public decimal? Maximum { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public bool? ExclusiveMaximum { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public decimal? Minimum { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public bool? ExclusiveMinimum { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public int? MaxLength { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public int? MinLength { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect + /// </summary> + public string Pattern { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public decimal? MultipleOf { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// The default value represents what would be assumed by the consumer of the input as the value of the schema if one is not provided. + /// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level. + /// For example, if type is string, then default can be "foo" but cannot be 1. + /// </summary> + public OpenApiAny Default { get; set; } + + /// <summary> + /// Relevant only for Schema "properties" definitions. Declares the property as "read only". + /// This means that it MAY be sent as part of a response but SHOULD NOT be sent as part of the request. + /// If the property is marked as readOnly being true and is in the required list, + /// the required will take effect on the response only. + /// A property MUST NOT be marked as both readOnly and writeOnly being true. + /// Default value is false. + /// </summary> + public bool ReadOnly { get; set; } + + /// <summary> + /// Relevant only for Schema "properties" definitions. Declares the property as "write only". + /// Therefore, it MAY be sent as part of a request but SHOULD NOT be sent as part of the response. + /// If the property is marked as writeOnly being true and is in the required list, + /// the required will take effect on the request only. + /// A property MUST NOT be marked as both readOnly and writeOnly being true. + /// Default value is false. + /// </summary> + public bool WriteOnly { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. + /// </summary> + public IList<OpenApiSchema> AllOf { get; set; } = new List<OpenApiSchema>(); + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. + /// </summary> + public IList<OpenApiSchema> OneOf { get; set; } = new List<OpenApiSchema>(); + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. + /// </summary> + public IList<OpenApiSchema> AnyOf { get; set; } = new List<OpenApiSchema>(); + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. + /// </summary> + public OpenApiSchema Not { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public ISet<string> Required { get; set; } = new HashSet<string>(); + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Value MUST be an object and not an array. Inline or referenced schema MUST be of a Schema Object + /// and not a standard JSON Schema. items MUST be present if the type is array. + /// </summary> + public OpenApiSchema Items { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public int? MaxItems { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public int? MinItems { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public bool? UniqueItems { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Property definitions MUST be a Schema Object and not a standard JSON Schema (inline or referenced). + /// </summary> + public IDictionary<string, OpenApiSchema> Properties { get; set; } = new Dictionary<string, OpenApiSchema>(); + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// PatternProperty definitions MUST be a Schema Object and not a standard JSON Schema (inline or referenced) + /// Each property name of this object SHOULD be a valid regular expression according to the ECMA 262 r + /// egular expression dialect. Each property value of this object MUST be an object, and each object MUST + /// be a valid Schema Object not a standard JSON Schema. + /// </summary> + public IDictionary<string, OpenApiSchema> PatternProperties { get; set; } = new Dictionary<string, OpenApiSchema>(); + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public int? MaxProperties { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public int? MinProperties { get; set; } + + /// <summary> + /// Indicates if the schema can contain properties other than those defined by the properties map. + /// </summary> + public bool AdditionalPropertiesAllowed { get; set; } = true; + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// Value can be boolean or object. Inline or referenced schema + /// MUST be of a Schema Object and not a standard JSON Schema. + /// </summary> + public OpenApiSchema AdditionalProperties { get; set; } + + /// <summary> + /// Adds support for polymorphism. The discriminator is an object name that is used to differentiate + /// between other schemas which may satisfy the payload description. + /// </summary> + public OpenApiDiscriminator Discriminator { get; set; } + + /// <summary> + /// A free-form property to include an example of an instance for this schema. + /// To represent examples that cannot be naturally represented in JSON or YAML, + /// a string value can be used to contain the example with escaping where necessary. + /// </summary> + public OpenApiAny Example { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public IList<JsonNode> Enum { get; set; } = new List<JsonNode>(); + + /// <summary> + /// Allows sending a null value for the defined schema. Default value is false. + /// </summary> + public bool Nullable { get; set; } + + /// <summary> + /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 + /// </summary> + public bool UnevaluatedProperties { get; set;} + + /// <summary> + /// Additional external documentation for this schema. + /// </summary> + public OpenApiExternalDocs ExternalDocs { get; set; } + + /// <summary> + /// Specifies that a schema is deprecated and SHOULD be transitioned out of usage. + /// Default value is false. + /// </summary> + public bool Deprecated { get; set; } + + /// <summary> + /// This MAY be used only on properties schemas. It has no effect on root schemas. + /// Adds additional metadata to describe the XML representation of this property. + /// </summary> + public OpenApiXml Xml { get; set; } + + /// <summary> + /// This object MAY be extended with Specification Extensions. + /// </summary> + public IDictionary<string, IOpenApiExtension> Extensions { get; set; } = new Dictionary<string, IOpenApiExtension>(); + + /// <summary> + /// Indicates object is a placeholder reference to an actual object and does not contain valid data. + /// </summary> + public bool UnresolvedReference { get; set; } + + /// <summary> + /// Reference object. + /// </summary> + public OpenApiReference Reference { get; set; } + + /// <summary> + /// Parameterless constructor + /// </summary> + public OpenApiSchema() { } + + /// <summary> + /// Initializes a copy of <see cref="OpenApiSchema"/> object + /// </summary> + public OpenApiSchema(OpenApiSchema schema) + { + Title = schema?.Title ?? Title; + Id = schema?.Id ?? Id; + Schema = schema?.Schema ?? Schema; + Comment = schema?.Comment ?? Comment; + Vocabulary = schema?.Vocabulary ?? Vocabulary; + DynamicAnchor = schema?.DynamicAnchor ?? DynamicAnchor; + DynamicRef = schema?.DynamicRef ?? DynamicRef; + RecursiveAnchor = schema?.RecursiveAnchor ?? RecursiveAnchor; + RecursiveRef = schema?.RecursiveRef ?? RecursiveRef; + Definitions = schema?.Definitions != null ? new Dictionary<string, OpenApiSchema>(schema.Definitions) : null; + UnevaluatedProperties = schema?.UnevaluatedProperties ?? UnevaluatedProperties; + V31ExclusiveMaximum = schema?.V31ExclusiveMaximum ?? V31ExclusiveMaximum; + V31ExclusiveMinimum = schema?.V31ExclusiveMinimum ?? V31ExclusiveMinimum; + Type = DeepCloneType(schema?.Type); + Format = schema?.Format ?? Format; + Description = schema?.Description ?? Description; + Maximum = schema?.Maximum ?? Maximum; + ExclusiveMaximum = schema?.ExclusiveMaximum ?? ExclusiveMaximum; + Minimum = schema?.Minimum ?? Minimum; + ExclusiveMinimum = schema?.ExclusiveMinimum ?? ExclusiveMinimum; + MaxLength = schema?.MaxLength ?? MaxLength; + MinLength = schema?.MinLength ?? MinLength; + Pattern = schema?.Pattern ?? Pattern; + MultipleOf = schema?.MultipleOf ?? MultipleOf; + Default = schema?.Default != null ? new(schema?.Default.Node) : null; + ReadOnly = schema?.ReadOnly ?? ReadOnly; + WriteOnly = schema?.WriteOnly ?? WriteOnly; + AllOf = schema?.AllOf != null ? new List<OpenApiSchema>(schema.AllOf) : null; + OneOf = schema?.OneOf != null ? new List<OpenApiSchema>(schema.OneOf) : null; + AnyOf = schema?.AnyOf != null ? new List<OpenApiSchema>(schema.AnyOf) : null; + Not = schema?.Not != null ? new(schema?.Not) : null; + Required = schema?.Required != null ? new HashSet<string>(schema.Required) : null; + Items = schema?.Items != null ? new(schema?.Items) : null; + MaxItems = schema?.MaxItems ?? MaxItems; + MinItems = schema?.MinItems ?? MinItems; + UniqueItems = schema?.UniqueItems ?? UniqueItems; + Properties = schema?.Properties != null ? new Dictionary<string, OpenApiSchema>(schema.Properties) : null; + PatternProperties = schema?.PatternProperties != null ? new Dictionary<string, OpenApiSchema>(schema.PatternProperties) : null; + MaxProperties = schema?.MaxProperties ?? MaxProperties; + MinProperties = schema?.MinProperties ?? MinProperties; + AdditionalPropertiesAllowed = schema?.AdditionalPropertiesAllowed ?? AdditionalPropertiesAllowed; + AdditionalProperties = schema?.AdditionalProperties != null ? new(schema?.AdditionalProperties) : null; + Discriminator = schema?.Discriminator != null ? new(schema?.Discriminator) : null; + Example = schema?.Example != null ? new(schema?.Example.Node) : null; + Enum = schema?.Enum != null ? new List<JsonNode>(schema.Enum) : null; + Nullable = schema?.Nullable ?? Nullable; + ExternalDocs = schema?.ExternalDocs != null ? new(schema?.ExternalDocs) : null; + Deprecated = schema?.Deprecated ?? Deprecated; + Xml = schema?.Xml != null ? new(schema?.Xml) : null; + Extensions = schema?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(schema.Extensions) : null; + UnresolvedReference = schema?.UnresolvedReference ?? UnresolvedReference; + Reference = schema?.Reference != null ? new(schema?.Reference) : null; + } + + /// <summary> + /// Serialize <see cref="OpenApiParameter"/> to Open Api v3.1 + /// </summary> + public virtual void SerializeAsV31(IOpenApiWriter writer) + { + SerializeInternal(writer, (writer, element) => element.SerializeAsV31(writer), + (writer, element) => element.SerializeAsV31WithoutReference(writer)); + } + + /// <summary> + /// Serialize <see cref="OpenApiParameter"/> to Open Api v3.0 + /// </summary> + public virtual void SerializeAsV3(IOpenApiWriter writer) + { + SerializeInternal(writer, (writer, element) => element.SerializeAsV3(writer), + (writer, element) => element.SerializeAsV3WithoutReference(writer)); + } + + private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter, IOpenApiSerializable> callback, + Action<IOpenApiWriter, IOpenApiReferenceable> action) + { + Utils.CheckArgumentNull(writer); + var target = this; + action(writer, target); + } + + /// <summary> + /// Serialize to OpenAPI V3 document without using reference. + /// </summary> + public virtual void SerializeAsV31WithoutReference(IOpenApiWriter writer) + { + SerializeInternalWithoutReference(writer, OpenApiSpecVersion.OpenApi3_1, + (writer, element) => element.SerializeAsV31(writer)); + } + + /// <summary> + /// Serialize to OpenAPI V3 document without using reference. + /// </summary> + public virtual void SerializeAsV3WithoutReference(IOpenApiWriter writer) + { + SerializeInternalWithoutReference(writer, OpenApiSpecVersion.OpenApi3_0, + (writer, element) => element.SerializeAsV3(writer)); + } + +/// <inheritdoc/> + + public void SerializeInternalWithoutReference(IOpenApiWriter writer, OpenApiSpecVersion version, + Action<IOpenApiWriter, IOpenApiSerializable> callback) + { + writer.WriteStartObject(); + + if (version == OpenApiSpecVersion.OpenApi3_1) + { + WriteV31Properties(writer); + } + + // title + writer.WriteProperty(OpenApiConstants.Title, Title); + + // multipleOf + writer.WriteProperty(OpenApiConstants.MultipleOf, MultipleOf); + + // maximum + writer.WriteProperty(OpenApiConstants.Maximum, Maximum); + + // exclusiveMaximum + writer.WriteProperty(OpenApiConstants.ExclusiveMaximum, ExclusiveMaximum); + + // minimum + writer.WriteProperty(OpenApiConstants.Minimum, Minimum); + + // exclusiveMinimum + writer.WriteProperty(OpenApiConstants.ExclusiveMinimum, ExclusiveMinimum); + + // maxLength + writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength); + + // minLength + writer.WriteProperty(OpenApiConstants.MinLength, MinLength); + + // pattern + writer.WriteProperty(OpenApiConstants.Pattern, Pattern); + + // maxItems + writer.WriteProperty(OpenApiConstants.MaxItems, MaxItems); + + // minItems + writer.WriteProperty(OpenApiConstants.MinItems, MinItems); + + // uniqueItems + writer.WriteProperty(OpenApiConstants.UniqueItems, UniqueItems); + + // maxProperties + writer.WriteProperty(OpenApiConstants.MaxProperties, MaxProperties); + + // minProperties + writer.WriteProperty(OpenApiConstants.MinProperties, MinProperties); + + // required + writer.WriteOptionalCollection(OpenApiConstants.Required, Required, (w, s) => w.WriteValue(s)); + + // enum + writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (nodeWriter, s) => nodeWriter.WriteAny(new OpenApiAny(s))); + + // type + if (Type.GetType() == typeof(string)) + { + writer.WriteProperty(OpenApiConstants.Type, (string)Type); + } + else + { + writer.WriteOptionalCollection(OpenApiConstants.Type, (string[])Type, (w, s) => w.WriteRaw(s)); + } + + // allOf + writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV3(w)); + + // anyOf + writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, (w, s) => s.SerializeAsV3(w)); + + // oneOf + writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, (w, s) => s.SerializeAsV3(w)); + + // not + writer.WriteOptionalObject(OpenApiConstants.Not, Not, (w, s) => s.SerializeAsV3(w)); + + // items + writer.WriteOptionalObject(OpenApiConstants.Items, Items, (w, s) => s.SerializeAsV3(w)); + + // properties + writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, (w, s) => s.SerializeAsV3(w)); + + // additionalProperties + if (AdditionalPropertiesAllowed) + { + writer.WriteOptionalObject( + OpenApiConstants.AdditionalProperties, + AdditionalProperties, + (w, s) => s.SerializeAsV3(w)); + } + else + { + writer.WriteProperty(OpenApiConstants.AdditionalProperties, AdditionalPropertiesAllowed); + } + + // description + writer.WriteProperty(OpenApiConstants.Description, Description); + + // format + writer.WriteProperty(OpenApiConstants.Format, Format); + + // default + writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d)); + + // nullable + writer.WriteProperty(OpenApiConstants.Nullable, Nullable, false); + + // discriminator + writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, (w, s) => s.SerializeAsV3(w)); + + // readOnly + writer.WriteProperty(OpenApiConstants.ReadOnly, ReadOnly, false); + + // writeOnly + writer.WriteProperty(OpenApiConstants.WriteOnly, WriteOnly, false); + + // xml + writer.WriteOptionalObject(OpenApiConstants.Xml, Xml, (w, s) => s.SerializeAsV2(w)); + + // externalDocs + writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV3(w)); + + // example + writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e)); + + // deprecated + writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false); + + // extensions + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); + + writer.WriteEndObject(); + } + +/// <inheritdoc/> + + public void SerializeAsV2WithoutReference(IOpenApiWriter writer) + { + SerializeAsV2WithoutReference( + writer: writer, + parentRequiredProperties: new HashSet<string>(), + propertyName: null); + } + +/// <inheritdoc/> + + public void SerializeAsV2(IOpenApiWriter writer) + { + SerializeAsV2(writer: writer, parentRequiredProperties: new HashSet<string>(), propertyName: null); + } + + internal void WriteV31Properties(IOpenApiWriter writer) + { + writer.WriteProperty(OpenApiConstants.Id, Id); + writer.WriteProperty(OpenApiConstants.DollarSchema, Schema); + writer.WriteProperty(OpenApiConstants.Comment, Comment); + writer.WriteProperty(OpenApiConstants.Vocabulary, Vocabulary); + writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV3(w)); + writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef); + writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor); + writer.WriteProperty(OpenApiConstants.RecursiveAnchor, RecursiveAnchor); + writer.WriteProperty(OpenApiConstants.RecursiveRef, RecursiveRef); + writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum); + writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum); + writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false); + writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w)); + } + + /// <summary> + /// Serialize <see cref="OpenApiSchema"/> to Open Api v2.0 and handles not marking the provided property + /// as readonly if its included in the provided list of required properties of parent schema. + /// </summary> + /// <param name="writer">The open api writer.</param> + /// <param name="parentRequiredProperties">The list of required properties in parent schema.</param> + /// <param name="propertyName">The property name that will be serialized.</param> + internal void SerializeAsV2( + IOpenApiWriter writer, + ISet<string> parentRequiredProperties, + string propertyName) + { + var target = this; + parentRequiredProperties ??= new HashSet<string>(); + + target.SerializeAsV2WithoutReference(writer, parentRequiredProperties, propertyName); + } + + /// <summary> + /// Serialize to OpenAPI V2 document without using reference and handles not marking the provided property + /// as readonly if its included in the provided list of required properties of parent schema. + /// </summary> + /// <param name="writer">The open api writer.</param> + /// <param name="parentRequiredProperties">The list of required properties in parent schema.</param> + /// <param name="propertyName">The property name that will be serialized.</param> + internal void SerializeAsV2WithoutReference( + IOpenApiWriter writer, + ISet<string> parentRequiredProperties, + string propertyName) + { + writer.WriteStartObject(); + WriteAsSchemaProperties(writer, parentRequiredProperties, propertyName); + writer.WriteEndObject(); + } + + internal void WriteAsSchemaProperties( + IOpenApiWriter writer, + ISet<string> parentRequiredProperties, + string propertyName) + { + // format + if (string.IsNullOrEmpty(Format)) + { + Format = AllOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ?? + AnyOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ?? + OneOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format; + } + + writer.WriteProperty(OpenApiConstants.Format, Format); + + // title + writer.WriteProperty(OpenApiConstants.Title, Title); + + // description + writer.WriteProperty(OpenApiConstants.Description, Description); + + // default + writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d)); + + // multipleOf + writer.WriteProperty(OpenApiConstants.MultipleOf, MultipleOf); + + // maximum + writer.WriteProperty(OpenApiConstants.Maximum, Maximum); + + // exclusiveMaximum + writer.WriteProperty(OpenApiConstants.ExclusiveMaximum, ExclusiveMaximum); + + // minimum + writer.WriteProperty(OpenApiConstants.Minimum, Minimum); + + // exclusiveMinimum + writer.WriteProperty(OpenApiConstants.ExclusiveMinimum, ExclusiveMinimum); + + // maxLength + writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength); + + // minLength + writer.WriteProperty(OpenApiConstants.MinLength, MinLength); + + // pattern + writer.WriteProperty(OpenApiConstants.Pattern, Pattern); + + // maxItems + writer.WriteProperty(OpenApiConstants.MaxItems, MaxItems); + + // minItems + writer.WriteProperty(OpenApiConstants.MinItems, MinItems); + + // uniqueItems + writer.WriteProperty(OpenApiConstants.UniqueItems, UniqueItems); + + // maxProperties + writer.WriteProperty(OpenApiConstants.MaxProperties, MaxProperties); + + // minProperties + writer.WriteProperty(OpenApiConstants.MinProperties, MinProperties); + + // required + writer.WriteOptionalCollection(OpenApiConstants.Required, Required, (w, s) => w.WriteValue(s)); + + // enum + writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (w, s) => w.WriteAny(new OpenApiAny(s))); + + // type + writer.WriteProperty(OpenApiConstants.Type, (string)Type); + + // items + writer.WriteOptionalObject(OpenApiConstants.Items, Items, (w, s) => s.SerializeAsV2(w)); + + // allOf + writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV2(w)); + + // If there isn't already an allOf, and the schema contains a oneOf or anyOf write an allOf with the first + // schema in the list as an attempt to guess at a graceful downgrade situation. + if (AllOf == null || AllOf.Count == 0) + { + // anyOf (Not Supported in V2) - Write the first schema only as an allOf. + writer.WriteOptionalCollection(OpenApiConstants.AllOf, AnyOf?.Take(1), (w, s) => s.SerializeAsV2(w)); + + if (AnyOf == null || AnyOf.Count == 0) + { + // oneOf (Not Supported in V2) - Write the first schema only as an allOf. + writer.WriteOptionalCollection(OpenApiConstants.AllOf, OneOf?.Take(1), (w, s) => s.SerializeAsV2(w)); + } + } + + // properties + writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, (w, key, s) => + s.SerializeAsV2(w, Required, key)); + + // additionalProperties + if (AdditionalPropertiesAllowed) + { + writer.WriteOptionalObject( + OpenApiConstants.AdditionalProperties, + AdditionalProperties, + (w, s) => s.SerializeAsV2(w)); + } + else + { + writer.WriteProperty(OpenApiConstants.AdditionalProperties, AdditionalPropertiesAllowed); + } + + // discriminator + writer.WriteProperty(OpenApiConstants.Discriminator, Discriminator?.PropertyName); + + // readOnly + // In V2 schema if a property is part of required properties of parent schema, + // it cannot be marked as readonly. + if (!parentRequiredProperties.Contains(propertyName)) + { + writer.WriteProperty(name: OpenApiConstants.ReadOnly, value: ReadOnly, defaultValue: false); + } + + // xml + writer.WriteOptionalObject(OpenApiConstants.Xml, Xml, (w, s) => s.SerializeAsV2(w)); + + // externalDocs + writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV2(w)); + + // example + writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e)); + + // extensions + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); + } + + private object DeepCloneType(object type) + { + if (type == null) + return null; + + if (type is string) + { + return type; // Return the string as is + } + + if (type is Array array) + { + Type elementType = type.GetType().GetElementType(); + Array copiedArray = Array.CreateInstance(elementType, array.Length); + for (int i = 0; i < array?.Length; i++) + { + copiedArray.SetValue(DeepCloneType(array?.GetValue(i)), i); + } + return copiedArray; + } + + return null; + } + } +} diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/ParserHelper.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/ParserHelper.cs index 9dd05ebdd..030572f68 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/ParserHelper.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/ParserHelper.cs @@ -4,7 +4,7 @@ using System; using System.Globalization; -namespace Microsoft.OpenApi.Readers.ParseNodes +namespace Microsoft.OpenApi.Reader.ParseNodes { /// <summary> /// Useful tools to parse data diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs index 50b0321c7..54c584df2 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs @@ -212,7 +212,7 @@ public static OpenApiParameter LoadParameter(ParseNode node, OpenApiDocument hos return LoadParameter(node, false, hostDocument); } - public static OpenApiParameter LoadParameter(ParseNode node, bool loadRequestBody, OpenApiDocument hostDocument) + public static OpenApiParameter LoadParameter(ParseNode node, bool loadRequestBody, OpenApiDocument hostDocument = null) { // Reset the local variables every time this method is called. node.Context.SetTempStorage(TempStorageKeys.ParameterIsBodyOrFormData, false); diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs new file mode 100644 index 000000000..868ea2d32 --- /dev/null +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Reader.ParseNodes; + +namespace Microsoft.OpenApi.Reader.V2 +{ + /// <summary> + /// Class containing logic to deserialize Open API V2 document into + /// runtime Open API object model. + /// </summary> + internal static partial class OpenApiV2Deserializer + { + private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new() + { + { + "title", + (o, n, _) => o.Title = n.GetScalarValue() + }, + { + "multipleOf", + (o, n, _) => o.MultipleOf = decimal.Parse(n.GetScalarValue(), NumberStyles.Float, CultureInfo.InvariantCulture) + }, + { + "maximum", + (o, n,_) => o.Maximum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue) + }, + { + "exclusiveMaximum", + (o, n, _) => o.ExclusiveMaximum = bool.Parse(n.GetScalarValue()) + }, + { + "minimum", + (o, n, _) => o.Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue) + }, + { + "exclusiveMinimum", + (o, n, _) => o.ExclusiveMinimum = bool.Parse(n.GetScalarValue()) + }, + { + "maxLength", + (o, n, _) => o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minLength", + (o, n, _) => o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "pattern", + (o, n, _) => o.Pattern = n.GetScalarValue() + }, + { + "maxItems", + (o, n, _) => o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minItems", + (o, n, _) => o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "uniqueItems", + (o, n, _) => o.UniqueItems = bool.Parse(n.GetScalarValue()) + }, + { + "maxProperties", + (o, n, _) => o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minProperties", + (o, n, _) => o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "required", + (o, n, _) => o.Required = new HashSet<string>(n.CreateSimpleList((n2, p) => n2.GetScalarValue())) + }, + { + "enum", + (o, n, _) => o.Enum = n.CreateListOfAny() + }, + + { + "type", + (o, n, _) => o.Type = n.GetScalarValue() + }, + { + "allOf", + (o, n, t) => o.AllOf = n.CreateList(LoadOpenApiSchema, t) + }, + { + "items", + (o, n, _) => o.Items = LoadOpenApiSchema(n) + }, + { + "properties", + (o, n, t) => o.Properties = n.CreateMap(LoadOpenApiSchema, t) + }, + { + "additionalProperties", (o, n, _) => + { + if (n is ValueNode) + { + o.AdditionalPropertiesAllowed = bool.Parse(n.GetScalarValue()); + } + else + { + o.AdditionalProperties = LoadOpenApiSchema(n); + } + } + }, + { + "description", + (o, n, _) => o.Description = n.GetScalarValue() + }, + { + "format", + (o, n, _) => o.Format = n.GetScalarValue() + }, + { + "default", + (o, n, _) => o.Default = n.CreateAny() + }, + { + "discriminator", (o, n, _) => + { + o.Discriminator = new() + { + PropertyName = n.GetScalarValue() + }; + } + }, + { + "readOnly", + (o, n, _) => o.ReadOnly = bool.Parse(n.GetScalarValue()) + }, + { + "xml", + (o, n, _) => o.Xml = LoadXml(n) + }, + { + "externalDocs", + (o, n, _) => o.ExternalDocs = LoadExternalDocs(n) + }, + { + "example", + (o, n, _) => o.Example = n.CreateAny() + }, + }; + + private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new PatternFieldMap<OpenApiSchema> + { + {s => s.StartsWith("x-"), (o, p, n, _) => o.AddExtension(p, LoadExtension(p, n))} + }; + + public static OpenApiSchema LoadOpenApiSchema(ParseNode node, OpenApiDocument hostDocument = null) + { + var mapNode = node.CheckMapNode("schema"); + + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return mapNode.GetReferencedObject<OpenApiSchema>(ReferenceType.Schema, pointer); + } + + var schema = new OpenApiSchema(); + + foreach (var propertyNode in mapNode) + { + propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields); + } + + return schema; + } + } +} diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs new file mode 100644 index 000000000..51b427321 --- /dev/null +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader.ParseNodes; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.OpenApi.Reader.V3 +{ + /// <summary> + /// Class containing logic to deserialize Open API V3 document into + /// runtime Open API object model. + /// </summary> + internal static partial class OpenApiV3Deserializer + { + private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new() + { + { + "title", + (o, n, _) => o.Title = n.GetScalarValue() + }, + { + "multipleOf", + (o, n, _) => o.MultipleOf = decimal.Parse(n.GetScalarValue(), NumberStyles.Float, CultureInfo.InvariantCulture) + }, + { + "maximum", + (o, n, _) => o.Maximum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue) + }, + { + "exclusiveMaximum", + (o, n, _) => o.ExclusiveMaximum = bool.Parse(n.GetScalarValue()) + }, + { + "minimum", + (o, n, _) => o.Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue) + }, + { + "exclusiveMinimum", + (o, n, _) => o.ExclusiveMinimum = bool.Parse(n.GetScalarValue()) + }, + { + "maxLength", + (o, n, _) => o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minLength", + (o, n, _) => o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "pattern", + (o, n, _) => o.Pattern = n.GetScalarValue() + }, + { + "maxItems", + (o, n, _) => o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minItems", + (o, n, _) => o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "uniqueItems", + (o, n, _) => o.UniqueItems = bool.Parse(n.GetScalarValue()) + }, + { + "maxProperties", + (o, n, _) => o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minProperties", + (o, n, _) => o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "required", + (o, n, _) => o.Required = new HashSet<string>(n.CreateSimpleList((n2, p) => n2.GetScalarValue())) + }, + { + "enum", + (o, n, _) => o.Enum = n.CreateListOfAny() + }, + { + "type", + (o, n, _) => o.Type = n.GetScalarValue() + }, + { + "allOf", + (o, n, t) => o.AllOf = n.CreateList(LoadOpenApiSchema, t) + }, + { + "oneOf", + (o, n, _) => o.OneOf = n.CreateList(LoadOpenApiSchema) + }, + { + "anyOf", + (o, n, t) => o.AnyOf = n.CreateList(LoadOpenApiSchema, t) + }, + { + "not", + (o, n, _) => o.Not = LoadOpenApiSchema(n) + }, + { + "items", + (o, n, _) => o.Items = LoadOpenApiSchema(n) + }, + { + "properties", + (o, n, t) => o.Properties = n.CreateMap(LoadOpenApiSchema, t) + }, + { + "additionalProperties", (o, n, _) => + { + if (n is ValueNode) + { + o.AdditionalPropertiesAllowed = bool.Parse(n.GetScalarValue()); + } + else + { + o.AdditionalProperties = LoadOpenApiSchema(n); + } + } + }, + { + "description", + (o, n, _) => o.Description = n.GetScalarValue() + }, + { + "format", + (o, n, _) => o.Format = n.GetScalarValue() + }, + { + "default", + (o, n, _) => o.Default = n.CreateAny() + }, + { + "nullable", + (o, n, _) => o.Nullable = bool.Parse(n.GetScalarValue()) + }, + { + "discriminator", + (o, n, _) => o.Discriminator = LoadDiscriminator(n) + }, + { + "readOnly", + (o, n, _) => o.ReadOnly = bool.Parse(n.GetScalarValue()) + }, + { + "writeOnly", + (o, n, _) => o.WriteOnly = bool.Parse(n.GetScalarValue()) + }, + { + "xml", + (o, n, _) => o.Xml = LoadXml(n) + }, + { + "externalDocs", + (o, n, _) => o.ExternalDocs = LoadExternalDocs(n) + }, + { + "example", + (o, n, _) => o.Example = n.CreateAny() + }, + { + "deprecated", + (o, n, _) => o.Deprecated = bool.Parse(n.GetScalarValue()) + }, + }; + + private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new() + { + {s => s.StartsWith("x-"), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))} + }; + + public static OpenApiSchema LoadOpenApiSchema(ParseNode node, OpenApiDocument hostDocument = null) + { + var mapNode = node.CheckMapNode(OpenApiConstants.Schema); + + var pointer = mapNode.GetReferencePointer(); + + if (pointer != null) + { + return new() + { + UnresolvedReference = true, + Reference = node.Context.VersionService.ConvertToOpenApiReference(pointer, ReferenceType.Schema) + }; + } + + var schema = new OpenApiSchema(); + + foreach (var propertyNode in mapNode) + { + propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields); + } + + return schema; + } + } +} diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs new file mode 100644 index 000000000..fa9d7dd93 --- /dev/null +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader.ParseNodes; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.OpenApi.Reader.V31 +{ + internal static partial class OpenApiV31Deserializer + { + private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new() + { + { + "title", + (o, n, _) => o.Title = n.GetScalarValue() + }, + { + "$schema", + (o, n, _) => o.Schema = n.GetScalarValue() + }, + { + "$id", + (o, n, _) => o.Id = n.GetScalarValue() + }, + { + "$comment", + (o, n, _) => o.Comment = n.GetScalarValue() + }, + { + "$vocabulary", + (o, n, _) => o.Vocabulary = n.GetScalarValue() + }, + { + "$dynamicRef", + (o, n, _) => o.DynamicRef = n.GetScalarValue() + }, + { + "$dynamicAnchor", + (o, n, _) => o.DynamicAnchor = n.GetScalarValue() + }, + { + "$recursiveAnchor", + (o, n, _) => o.RecursiveAnchor = n.GetScalarValue() + }, + { + "$recursiveRef", + (o, n, _) => o.RecursiveRef = n.GetScalarValue() + }, + { + "$defs", + (o, n, t) => o.Definitions = n.CreateMap(LoadOpenApiSchema, t) + }, + { + "multipleOf", + (o, n, _) => o.MultipleOf = decimal.Parse(n.GetScalarValue(), NumberStyles.Float, CultureInfo.InvariantCulture) + }, + { + "maximum", + (o, n, _) => o.Maximum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue) + }, + { + "exclusiveMaximum", + (o, n, _) => o.V31ExclusiveMaximum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue) + }, + { + "minimum", + (o, n, _) => o.Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue) + }, + { + "exclusiveMinimum", + (o, n, _) => o.V31ExclusiveMinimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue) + }, + { + "maxLength", + (o, n, _) => o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minLength", + (o, n, _) => o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "pattern", + (o, n, _) => o.Pattern = n.GetScalarValue() + }, + { + "maxItems", + (o, n, _) => o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minItems", + (o, n, _) => o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "uniqueItems", + (o, n, _) => o.UniqueItems = bool.Parse(n.GetScalarValue()) + }, + { + "unevaluatedProperties", + (o, n, _) => o.UnevaluatedProperties = bool.Parse(n.GetScalarValue()) + }, + { + "maxProperties", + (o, n, _) => o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "minProperties", + (o, n, _) => o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture) + }, + { + "required", + (o, n, _) => o.Required = new HashSet<string>(n.CreateSimpleList((n2, p) => n2.GetScalarValue())) + }, + { + "enum", + (o, n, _) => o.Enum = n.CreateListOfAny() + }, + { + "type", + (o, n, _) => + { + o.Type = n is ValueNode + ? n.GetScalarValue() + : n.CreateSimpleList((n2, p) => n2.GetScalarValue()).ToArray(); + } + }, + { + "allOf", + (o, n, t) => o.AllOf = n.CreateList(LoadOpenApiSchema, t) + }, + { + "oneOf", + (o, n, t) => o.OneOf = n.CreateList(LoadOpenApiSchema, t) + }, + { + "anyOf", + (o, n, t) => o.AnyOf = n.CreateList(LoadOpenApiSchema, t) + }, + { + "not", + (o, n, _) => o.Not = LoadOpenApiSchema(n) + }, + { + "items", + (o, n, _) => o.Items = LoadOpenApiSchema(n) + }, + { + "properties", + (o, n, t) => o.Properties = n.CreateMap(LoadOpenApiSchema, t) + }, + { + "patternProperties", + (o, n, t) => o.PatternProperties = n.CreateMap(LoadOpenApiSchema, t) + }, + { + "additionalProperties", (o, n, _) => + { + if (n is ValueNode) + { + o.AdditionalPropertiesAllowed = bool.Parse(n.GetScalarValue()); + } + else + { + o.AdditionalProperties = LoadOpenApiSchema(n); + } + } + }, + { + "description", + (o, n, _) => o.Description = n.GetScalarValue() + }, + { + "format", + (o, n, _) => o.Format = n.GetScalarValue() + }, + { + "default", + (o, n, _) => o.Default = n.CreateAny() + }, + { + "nullable", + (o, n, _) => o.Nullable = bool.Parse(n.GetScalarValue()) + }, + { + "discriminator", + (o, n, _) => o.Discriminator = LoadDiscriminator(n) + }, + { + "readOnly", + (o, n, _) => o.ReadOnly = bool.Parse(n.GetScalarValue()) + }, + { + "writeOnly", + (o, n, _) => o.WriteOnly = bool.Parse(n.GetScalarValue()) + }, + { + "xml", + (o, n, _) => o.Xml = LoadXml(n) + }, + { + "externalDocs", + (o, n, _) => o.ExternalDocs = LoadExternalDocs(n) + }, + { + "example", + (o, n, _) => o.Example = n.CreateAny() + }, + { + "deprecated", + (o, n, _) => o.Deprecated = bool.Parse(n.GetScalarValue()) + }, + }; + + private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new() + { + {s => s.StartsWith("x-"), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))} + }; + + public static OpenApiSchema LoadOpenApiSchema(ParseNode node, OpenApiDocument hostDocument = null) + { + var mapNode = node.CheckMapNode(OpenApiConstants.Schema); + + var pointer = mapNode.GetReferencePointer(); + + if (pointer != null) + { + return new() + { + UnresolvedReference = true, + Reference = node.Context.VersionService.ConvertToOpenApiReference(pointer, ReferenceType.Schema) + }; + } + + var schema = new OpenApiSchema(); + + foreach (var propertyNode in mapNode) + { + propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields); + } + + return schema; + } + } +} diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs index 202e4e905..5e47f03b6 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs @@ -57,6 +57,7 @@ public OpenApiV31VersionService(OpenApiDiagnostic diagnostic) [typeof(OpenApiResponse)] = OpenApiV31Deserializer.LoadResponse, [typeof(OpenApiResponses)] = OpenApiV31Deserializer.LoadResponses, [typeof(JsonSchema)] = OpenApiV31Deserializer.LoadSchema, + [typeof(OpenApiSchema)] = OpenApiV31Deserializer.LoadOpenApiSchema, [typeof(OpenApiSecurityRequirement)] = OpenApiV31Deserializer.LoadSecurityRequirement, [typeof(OpenApiSecurityScheme)] = OpenApiV31Deserializer.LoadSecurityScheme, [typeof(OpenApiServer)] = OpenApiV31Deserializer.LoadServer, diff --git a/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/ParserHelperTests.cs b/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/ParserHelperTests.cs index 1368e103d..4e3500d6b 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/ParserHelperTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/ParserHelperTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using System.Globalization; -using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Reader.ParseNodes; using Xunit; namespace Microsoft.OpenApi.Readers.Tests.ParseNodes diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs new file mode 100644 index 000000000..72c5289e5 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V31Tests +{ + public class OpenApiSchemaTests + { + private const string SampleFolderPath = "V31Tests/Samples/OpenApiSchema/"; + + [Fact] + public void ParseBasicV31SchemaShouldSucceed() + { + var expectedObject = new OpenApiSchema() + { + Id = "https://example.com/arrays.schema.json", + Schema = "https://json-schema.org/draft/2020-12/schema", + Description = "A representation of a person, company, organization, or place", + Type = "object", + Properties = new Dictionary<string, OpenApiSchema> + { + ["fruits"] = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string" + } + }, + ["vegetables"] = new OpenApiSchema + { + Type = "array" + } + }, + Definitions = new Dictionary<string, OpenApiSchema> + { + ["veggie"] = new OpenApiSchema + { + Type = "object", + Required = new HashSet<string> + { + "veggieName", + "veggieLike" + }, + Properties = new Dictionary<string, OpenApiSchema> + { + ["veggieName"] = new OpenApiSchema + { + Type = "string", + Description = "The name of the vegetable." + }, + ["veggieLike"] = new OpenApiSchema + { + Type = "boolean", + Description = "Do I like this vegetable?" + } + } + } + } + }; + + // Act + var schema = OpenApiModelFactory.Load<OpenApiSchema>( + Path.Combine(SampleFolderPath, "jsonSchema.json"), OpenApiSpecVersion.OpenApi3_1, out _); + + // Assert + schema.Should().BeEquivalentTo(expectedObject); + } + + [Fact] + public void ParseSchemaWithTypeArrayWorks() + { + // Arrange + var schema = @"{ + ""$id"": ""https://example.com/arrays.schema.json"", + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""description"": ""A representation of a person, company, organization, or place"", + ""type"": [""object"", ""null""] +}"; + + var expected = new OpenApiSchema() + { + Id = "https://example.com/arrays.schema.json", + Schema = "https://json-schema.org/draft/2020-12/schema", + Description = "A representation of a person, company, organization, or place", + Type = new string[] { "object", "null" } + }; + + // Act + var actual = OpenApiModelFactory.Parse<OpenApiSchema>(schema, OpenApiSpecVersion.OpenApi3_1, out _); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void TestSchemaCopyConstructorWithTypeArrayWorks() + { + /* Arrange + * Test schema's copy constructor for deep-cloning type array + */ + var schemaWithTypeArray = new OpenApiSchema() + { + Type = new string[] { "array", "null" }, + Items = new OpenApiSchema + { + Type = "string" + } + }; + + var simpleSchema = new OpenApiSchema() + { + Type = "string" + }; + + // Act + var schemaWithArrayCopy = new OpenApiSchema(schemaWithTypeArray); + schemaWithArrayCopy.Type = "string"; + + var simpleSchemaCopy = new OpenApiSchema(simpleSchema); + simpleSchemaCopy.Type = new string[] { "string", "null" }; + + // Assert + schemaWithArrayCopy.Type.Should().NotBeEquivalentTo(schemaWithTypeArray.Type); + schemaWithTypeArray.Type = new string[] { "string", "null" }; + + simpleSchemaCopy.Type.Should().NotBeEquivalentTo(simpleSchema.Type); + simpleSchema.Type = "string"; + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json new file mode 100644 index 000000000..84b1ea211 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json @@ -0,0 +1,33 @@ +{ + "$id": "https://example.com/arrays.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A representation of a person, company, organization, or place", + "type": "object", + "properties": { + "fruits": { + "type": "array", + "items": { + "type": "string" + } + }, + "vegetables": { + "type": "array" + } + }, + "$defs": { + "veggie": { + "type": "object", + "required": [ "veggieName", "veggieLike" ], + "properties": { + "veggieName": { + "type": "string", + "description": "The name of the vegetable." + }, + "veggieLike": { + "type": "boolean", + "description": "Do I like this vegetable?" + } + } + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs new file mode 100644 index 000000000..b67f64de1 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; +using Xunit; +using FluentAssertions; +using Microsoft.OpenApi.Extensions; + +namespace Microsoft.OpenApi.Tests.Models +{ + public class OpenApiSchemaTests + { + public static OpenApiSchema BasicV31Schema = new() + { + Id = "https://example.com/arrays.schema.json", + Schema = "https://json-schema.org/draft/2020-12/schema", + Description = "A representation of a person, company, organization, or place", + Type = "object", + Properties = new Dictionary<string, OpenApiSchema> + { + ["fruits"] = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string" + } + }, + ["vegetables"] = new OpenApiSchema + { + Type = "array" + } + }, + Definitions = new Dictionary<string, OpenApiSchema> + { + ["veggie"] = new OpenApiSchema + { + Type = "object", + Required = new HashSet<string>{ "veggieName", "veggieLike" }, + Properties = new Dictionary<string, OpenApiSchema> + { + ["veggieName"] = new OpenApiSchema + { + Type = "string", + Description = "The name of the vegetable." + }, + ["veggieLike"] = new OpenApiSchema + { + Type = "boolean", + Description = "Do I like this vegetable?" + } + } + } + } + }; + + [Fact] + public void SerializeBasicV31SchemaWorks() + { + // Arrange + var expected = @"{ + ""$id"": ""https://example.com/arrays.schema.json"", + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""$defs"": { + ""veggie"": { + ""required"": [ + ""veggieName"", + ""veggieLike"" + ], + ""type"": ""object"", + ""properties"": { + ""veggieName"": { + ""type"": ""string"", + ""description"": ""The name of the vegetable."" + }, + ""veggieLike"": { + ""type"": ""boolean"", + ""description"": ""Do I like this vegetable?"" + } + } + } + }, + ""type"": ""object"", + ""properties"": { + ""fruits"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + }, + ""vegetables"": { + ""type"": ""array"" + } + }, + ""description"": ""A representation of a person, company, organization, or place"" +}"; + + // Act + var actual = BasicV31Schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_1); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 7e0730600..58d7a576e 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -459,6 +459,7 @@ namespace Microsoft.OpenApi.Models public const string BodyName = "x-bodyName"; public const string Callbacks = "callbacks"; public const string ClientCredentials = "clientCredentials"; + public const string Comment = "$comment"; public const string Components = "components"; public const string ComponentsSegment = "/components/"; public const string Consumes = "consumes"; @@ -471,11 +472,15 @@ namespace Microsoft.OpenApi.Models public const string DefaultName = "Default Name"; public const string DefaultTitle = "Default Title"; public const string Definitions = "definitions"; + public const string Defs = "$defs"; public const string Delete = "delete"; public const string Deprecated = "deprecated"; public const string Description = "description"; public const string Discriminator = "discriminator"; public const string DollarRef = "$ref"; + public const string DollarSchema = "$schema"; + public const string DynamicAnchor = "$dynamicAnchor"; + public const string DynamicRef = "$dynamicRef"; public const string Email = "email"; public const string Encoding = "encoding"; public const string Enum = "enum"; @@ -495,6 +500,7 @@ namespace Microsoft.OpenApi.Models public const string Head = "head"; public const string Headers = "headers"; public const string Host = "host"; + public const string Id = "$id"; public const string Identifier = "identifier"; public const string Implicit = "implicit"; public const string In = "in"; @@ -539,6 +545,8 @@ namespace Microsoft.OpenApi.Models public const string PropertyName = "propertyName"; public const string Put = "put"; public const string ReadOnly = "readOnly"; + public const string RecursiveAnchor = "$recursiveAnchor"; + public const string RecursiveRef = "$recursiveRef"; public const string RefreshUrl = "refreshUrl"; public const string RequestBodies = "requestBodies"; public const string RequestBody = "requestBody"; @@ -563,13 +571,17 @@ namespace Microsoft.OpenApi.Models public const string TokenUrl = "tokenUrl"; public const string Trace = "trace"; public const string Type = "type"; + public const string UnevaluatedProperties = "unevaluatedProperties"; public const string UniqueItems = "uniqueItems"; public const string Url = "url"; public const string V2ReferenceUri = "https://registry/definitions/"; + public const string V31ExclusiveMaximum = "exclusiveMaximum"; + public const string V31ExclusiveMinimum = "exclusiveMinimum"; public const string V3ReferenceUri = "https://registry/components/schemas/"; public const string Value = "value"; public const string Variables = "variables"; public const string Version = "version"; + public const string Vocabulary = "$vocabulary"; public const string Webhooks = "webhooks"; public const string Wrapped = "wrapped"; public const string WriteOnly = "writeOnly"; @@ -945,6 +957,71 @@ namespace Microsoft.OpenApi.Models public OpenApiResponses() { } public OpenApiResponses(Microsoft.OpenApi.Models.OpenApiResponses openApiResponses) { } } + public class OpenApiSchema : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable + { + public OpenApiSchema() { } + public OpenApiSchema(Microsoft.OpenApi.Models.OpenApiSchema schema) { } + public Microsoft.OpenApi.Models.OpenApiSchema AdditionalProperties { get; set; } + public bool AdditionalPropertiesAllowed { get; set; } + public System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiSchema> AllOf { get; set; } + public System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiSchema> AnyOf { get; set; } + public string Comment { get; set; } + public Microsoft.OpenApi.Any.OpenApiAny Default { get; set; } + public System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> Definitions { get; set; } + public bool Deprecated { get; set; } + public string Description { get; set; } + public Microsoft.OpenApi.Models.OpenApiDiscriminator Discriminator { get; set; } + public string DynamicAnchor { get; set; } + public string DynamicRef { get; set; } + public System.Collections.Generic.IList<System.Text.Json.Nodes.JsonNode> Enum { get; set; } + public Microsoft.OpenApi.Any.OpenApiAny Example { get; set; } + public bool? ExclusiveMaximum { get; set; } + public bool? ExclusiveMinimum { get; set; } + public System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Interfaces.IOpenApiExtension> Extensions { get; set; } + public Microsoft.OpenApi.Models.OpenApiExternalDocs ExternalDocs { get; set; } + public string Format { get; set; } + public string Id { get; set; } + public Microsoft.OpenApi.Models.OpenApiSchema Items { get; set; } + public int? MaxItems { get; set; } + public int? MaxLength { get; set; } + public int? MaxProperties { get; set; } + public decimal? Maximum { get; set; } + public int? MinItems { get; set; } + public int? MinLength { get; set; } + public int? MinProperties { get; set; } + public decimal? Minimum { get; set; } + public decimal? MultipleOf { get; set; } + public Microsoft.OpenApi.Models.OpenApiSchema Not { get; set; } + public bool Nullable { get; set; } + public System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiSchema> OneOf { get; set; } + public string Pattern { get; set; } + public System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> PatternProperties { get; set; } + public System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> Properties { get; set; } + public bool ReadOnly { get; set; } + public string RecursiveAnchor { get; set; } + public string RecursiveRef { get; set; } + public Microsoft.OpenApi.Models.OpenApiReference Reference { get; set; } + public System.Collections.Generic.ISet<string> Required { get; set; } + public string Schema { get; set; } + public string Title { get; set; } + public object Type { get; set; } + public bool UnEvaluatedProperties { get; set; } + public bool UnevaluatedProperties { get; set; } + public bool? UniqueItems { get; set; } + public bool UnresolvedReference { get; set; } + public decimal? V31ExclusiveMaximum { get; set; } + public decimal? V31ExclusiveMinimum { get; set; } + public string Vocabulary { get; set; } + public bool WriteOnly { get; set; } + public Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; } + public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public void SerializeAsV2WithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public virtual void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public virtual void SerializeAsV31(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public virtual void SerializeAsV31WithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public virtual void SerializeAsV3WithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public void SerializeInternalWithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer, Microsoft.OpenApi.OpenApiSpecVersion version, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, Microsoft.OpenApi.Interfaces.IOpenApiSerializable> callback) { } + } public class OpenApiSecurityRequirement : System.Collections.Generic.Dictionary<Microsoft.OpenApi.Models.OpenApiSecurityScheme, System.Collections.Generic.IList<string>>, Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiSerializable { public OpenApiSecurityRequirement() { }