Repository: getkin/kin-openapi
Pull request #1024 (2024-10-23 12:01:46)

openapi3: reference originating locations in YAML specs - step 2
This PR adds the `origin` field to two structs that were missed in step 1:
- Example
- XML

In addition, a new unit test demonstrates an edge case that causes a superfluous `origin` key in openapi3.Example.Value when the example is defined as a json object.


Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 44ab4d6..a1631df 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -468,6 +468,7 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding
 
 type Example struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
+	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
 	Summary       string `json:"summary,omitempty" yaml:"summary,omitempty"`
 	Description   string `json:"description,omitempty" yaml:"description,omitempty"`
@@ -2240,6 +2241,7 @@ type ValidationOptions struct {
 
 type XML struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
+	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
 	Name      string `json:"name,omitempty" yaml:"name,omitempty"`
 	Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
diff --git a/openapi3/example.go b/openapi3/example.go
index 9d38e43..56aee37 100644
--- a/openapi3/example.go
+++ b/openapi3/example.go
@@ -10,6 +10,7 @@ import (
 // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object
 type Example struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
+	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
 	Summary       string `json:"summary,omitempty" yaml:"summary,omitempty"`
 	Description   string `json:"description,omitempty" yaml:"description,omitempty"`
@@ -59,6 +60,7 @@ func (example *Example) UnmarshalJSON(data []byte) error {
 		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
+	delete(x.Extensions, originKey)
 	delete(x.Extensions, "summary")
 	delete(x.Extensions, "description")
 	delete(x.Extensions, "value")
diff --git a/openapi3/xml.go b/openapi3/xml.go
index 69d1b34..7acd818 100644
--- a/openapi3/xml.go
+++ b/openapi3/xml.go
@@ -9,6 +9,7 @@ import (
 // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object
 type XML struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
+	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
 	Name      string `json:"name,omitempty" yaml:"name,omitempty"`
 	Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
@@ -58,6 +59,7 @@ func (xml *XML) UnmarshalJSON(data []byte) error {
 		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
+	delete(x.Extensions, originKey)
 	delete(x.Extensions, "name")
 	delete(x.Extensions, "namespace")
 	delete(x.Extensions, "prefix")


Test patch:
diff --git a/openapi3/origin_test.go b/openapi3/origin_test.go
index 56d8f1d..d955056 100644
--- a/openapi3/origin_test.go
+++ b/openapi3/origin_test.go
@@ -2,33 +2,11 @@ package openapi3
 
 import (
 	"context"
-	"fmt"
-	"os"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 )
 
-func TestOrigin_All(t *testing.T) {
-	loader := NewLoader()
-	loader.IsExternalRefsAllowed = true
-	loader.IncludeOrigin = true
-	loader.Context = context.Background()
-
-	const dir = "testdata/origin/"
-	items, _ := os.ReadDir(dir)
-	for _, item := range items {
-		t.Run(item.Name(), func(t *testing.T) {
-			doc, err := loader.LoadFromFile(fmt.Sprintf("%s/%s", dir, item.Name()))
-			require.NoError(t, err)
-			if doc.Paths == nil {
-				t.Skip("no paths")
-			}
-			require.NotEmpty(t, doc.Paths.Origin)
-		})
-	}
-}
-
 func TestOrigin_Info(t *testing.T) {
 	loader := NewLoader()
 	loader.IsExternalRefsAllowed = true
@@ -301,3 +279,72 @@ func TestOrigin_Security(t *testing.T) {
 		},
 		base.Flows.Implicit.Origin.Fields["authorizationUrl"])
 }
+
+func TestOrigin_Example(t *testing.T) {
+	loader := NewLoader()
+	loader.IsExternalRefsAllowed = true
+	loader.IncludeOrigin = true
+	loader.Context = context.Background()
+
+	doc, err := loader.LoadFromFile("testdata/origin/example.yaml")
+	require.NoError(t, err)
+
+	base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Examples["bar"].Value
+	require.NotNil(t, base.Origin)
+	require.Equal(t,
+		&Location{
+			Line:   14,
+			Column: 15,
+		},
+		base.Origin.Key)
+
+	require.Equal(t,
+		Location{
+			Line:   15,
+			Column: 17,
+		},
+		base.Origin.Fields["summary"])
+
+	//	Note:
+	//  Example.Value contains an extra field: "origin".
+	//
+	//	Explanation:
+	//  The example value is defined in the original yaml file as a json object: {"bar": "baz"}
+	//  This json object is also valid in YAML, so yaml.3 decodes it as a map and adds an "origin" field.
+	require.Contains(t,
+		base.Value,
+		originKey)
+}
+
+func TestOrigin_XML(t *testing.T) {
+	loader := NewLoader()
+	loader.IsExternalRefsAllowed = true
+	loader.IncludeOrigin = true
+	loader.Context = context.Background()
+
+	doc, err := loader.LoadFromFile("testdata/origin/xml.yaml")
+	require.NoError(t, err)
+
+	base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["name"].Value.XML
+	require.NotNil(t, base.Origin)
+	require.Equal(t,
+		&Location{
+			Line:   21,
+			Column: 19,
+		},
+		base.Origin.Key)
+
+	require.Equal(t,
+		Location{
+			Line:   22,
+			Column: 21,
+		},
+		base.Origin.Fields["namespace"])
+
+	require.Equal(t,
+		Location{
+			Line:   23,
+			Column: 21,
+		},
+		base.Origin.Fields["prefix"])
+}
diff --git a/openapi3/testdata/origin/example.yaml b/openapi3/testdata/origin/example.yaml
new file mode 100644
index 0000000..8b50ef2
--- /dev/null
+++ b/openapi3/testdata/origin/example.yaml
@@ -0,0 +1,19 @@
+openapi: 3.0.0
+info:
+  title: Security Requirement Example
+  version: 1.0.0
+paths:
+  /subscribe:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+            examples:
+              bar:
+                summary: A bar example
+                value: {"bar": "baz"}
+      responses:
+        "200":
+          description: OK
diff --git a/openapi3/testdata/origin/xml.yaml b/openapi3/testdata/origin/xml.yaml
new file mode 100644
index 0000000..3193f32
--- /dev/null
+++ b/openapi3/testdata/origin/xml.yaml
@@ -0,0 +1,26 @@
+openapi: 3.0.0
+info:
+  title: Security Requirement Example
+  version: 1.0.0
+paths:
+  /subscribe:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: integer
+                  format: int32
+                  xml:
+                    attribute: true
+                name:
+                  type: string
+                  xml:
+                    namespace: http://example.com/schema/sample
+                    prefix: sample
+      responses:
+        "200":
+          description: OK