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

YAML Fragment as alias nodes. #41 #47

Merged
merged 13 commits into from
Jun 20, 2022
38 changes: 28 additions & 10 deletions draft-ietf-httpapi-yaml-mediatypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ normative:
- ins: Mike Ralphson
- ins: Ron Ratovsky
- ins: Uri Sarid
JSON-POINTER: RFC6901
ioggstream marked this conversation as resolved.
Show resolved Hide resolved

informative:

Expand Down Expand Up @@ -109,36 +110,41 @@ in this document are to be interpreted as in {{!SEMANTICS=I-D.ietf-httpbis-seman
The terms "fragment" and "fragment identifier"
in this document are to be interpreted as in {{!URI=RFC3986}}.

The terms "node", "anchor" and "named anchor"
The terms "node", "alias node", "anchor" and "named anchor"
in this document are to be intepreded as in [YAML].

## Fragment identification {#application-yaml-fragment}

This section describes how to use
named anchors (see Section 3.2.2.2 of [YAML])
alias nodes (see Section 3.2.2.2 and 7.1 of [YAML])
as fragment identifiers to designate nodes.

A YAML named anchor can be represented in a URI fragment identifier
A YAML alias node can be represented in a URI fragment identifier
by encoding it into octects using UTF-8 {{!UTF-8=RFC3629}},
while percent-encoding those characters not allowed by the fragment rule
in {{Section 3.5 of URI}}.

If multiple nodes would match a fragment identifier,
the first such match is selected.

A fragment identifier is not guaranteed to reference an existing node.
Therefore, applications SHOULD define how an unresolved alias node
ought to be handled.

Users concerned with interoperability of fragment identifiers:

- SHOULD limit named anchors to a set of characters
- SHOULD limit alias nodes to a set of characters
that do not require encoding
to be expressed as URI fragment identifiers:
this is always possible since named anchors are a serialization
this is always possible since alias nodes
and their associated named anchors are a serialization
detail;
- SHOULD NOT use a named anchor that matches multiple nodes.
- SHOULD NOT use alias nodes that matches multiple nodes.
ioggstream marked this conversation as resolved.
Show resolved Hide resolved

In the example resource below, the URL `file.yaml#foo`
references the anchor `foo` pointing to the node with value `scalar`;
In the example resource below, the URL `file.yaml#*foo`
references the alias node `*foo` pointing to the node with value `scalar`;
whereas
the URL `file.yaml#bar` references the anchor `bar` pointing to the node
the URL `file.yaml#*bar` references the alias node `*bar` pointing to the node
with value `[ some, sequence, items ]`.

~~~ example
Expand Down Expand Up @@ -190,7 +196,11 @@ Applications that use this media type:
: HTTP

Fragment identifier considerations:
: see {{application-yaml-fragment}}
: A fragment identifier starting with "*"
is expressed as a YAML alias node {{application-yaml-fragment}}.
ioggstream marked this conversation as resolved.
Show resolved Hide resolved

A fragment identifier starting with "/"
is expressed as a JSON Pointer {{!JSON-POINTER}}.

Additional information:

Expand Down Expand Up @@ -434,6 +444,14 @@ Q: Why this document?
This has some security implications too
(eg. wrt on identifying parsers or treat downloads)

Q: Why using alias nodes as fragment identifiers?
: Alias nodes starts with `*`. This allow to distinguish
a fragment identifier expressed as an alias node from
one expressed in JSON Pointer {{JSON-POINTER}}
which is expected to start with `/`.
Moreover, since json-path {{I-D.ietf-jsonpath-base}} expressions
start with `$`, this mechanism is even extensible that specification.

# Change Log
{: numbered="false"}

Expand Down
90 changes: 85 additions & 5 deletions test_yaml_json.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# Roundtrip yaml/json.
from graphql import ValidationRule
from path import Path
from pathlib import Path
import yaml, json
import pytest

import logging
import abnf

testcases = yaml.safe_load(Path("yaml-json-interoperability.yaml").read_text())

import logging
fragment_identifier_testcases = yaml.safe_load(Path("yaml-fragment-identifiers.yaml").read_text())

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -50,3 +49,84 @@ def test_supported(testname, testcase):
data = testcase["data"]
ret = yaml.safe_load(data)
assert testcase["expected"] == json_safe_dump(ret)


from urllib.parse import urlparse, urlsplit, urlunsplit
from urllib.parse import quote, unquote


def iri_to_uri(iri, encoding="utf-8"):
"Takes a Unicode string that can contain an IRI and emits a URI."
scheme, authority, path, query, frag = urlsplit(iri)
scheme = scheme.encode(encoding)
if ":" in authority:
host, port = authority.split(":", 1)
authority = host.encode("idna") + f":{port}".encode()
else:
authority = authority.encode(encoding)
path = quote(path.encode(encoding), safe="/;%[]=:$&()+,!?*@'~")
query = quote(query.encode(encoding), safe="/;%[]=:$&()+,!?*@'~")
frag = quote(frag.encode(encoding), safe="/;%[]=:$&()+,!?*@'~")
return urlunsplit(
x.encode() if hasattr(x, "encode") else x
for x in (scheme, authority, path, query, frag)
)


@pytest.mark.parametrize(
"alias_node",
[
"*foo",
"*foo-bar-baz",
"*però",
"*però/fara",
"*però/fara/perì",
"/components/schemas/Person",
"$.o.*",
],
)
def test_uri_alias_nodes(alias_node):
"""
fragment syntax:
fragment = *( pchar / "/" / "?" )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
pct-encoded = "%" HEXDIG HEXDIG
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"

"""
s = "https://host.example:443/path.yaml#" + alias_node
url2 = iri_to_uri(s)
url = urlparse(url2)
print(
f'\n{{ "{alias_node}": {{ "iri": "{s}","url": "{url2.decode("""ascii""")}" }} }},'
)
fragment = unquote(url.fragment)
print(fragment)


@pytest.mark.parametrize("testcase", [
testcase for testcase in fragment_identifier_testcases["yaml-fragment-identifiers"]["data"]
])
def test_iri_full(testcase):
((alias_node, testcase),) = testcase.items()
url = urlparse(testcase["url"])
iri = urlparse(testcase["iri"])
parsed_fragment = unquote(url.fragment)
validate_uri_fragment(url.fragment)
iri_fragment = iri.fragment
assert parsed_fragment == iri_fragment


def validate_uri_fragment(uri_fragment):
rules = """
sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
pct-encoded = "%" HEXDIG HEXDIG
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
fragment = *( pchar / "/" / "?" )
"""
for rule in rules.strip().splitlines():
abnf.Rule.create(rule.strip())
return abnf.Rule('fragment').parse_all(uri_fragment)
45 changes: 45 additions & 0 deletions yaml-fragment-identifiers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
yaml-fragment-identifiers:
data: [
{
"*foo": {
"iri": "https://host.example:443/path.yaml#*foo",
"url": "https://host.example:443/path.yaml#*foo",
}
},
{
"*foo-bar-baz": {
"iri": "https://host.example:443/path.yaml#*foo-bar-baz",
"url": "https://host.example:443/path.yaml#*foo-bar-baz",
}
},
{
"*però": {
"iri": "https://host.example:443/path.yaml#*però",
"url": "https://host.example:443/path.yaml#*per%C3%B2",
}
},
{
"*però/fara": {
"iri": "https://host.example:443/path.yaml#*però/fara",
"url": "https://host.example:443/path.yaml#*per%C3%B2/fara",
}
},
{
"*però/fara/perì": {
"iri": "https://host.example:443/path.yaml#*però/fara/perì",
"url": "https://host.example:443/path.yaml#*per%C3%B2/fara/per%C3%AC",
}
},
{
"/components/schemas/Person": {
"iri": "https://host.example:443/path.yaml#/components/schemas/Person",
"url": "https://host.example:443/path.yaml#/components/schemas/Person",
}
},
{
"$.o.*": {
"iri": "https://host.example:443/path.yaml#$.o.*",
"url": "https://host.example:443/path.yaml#$.o.*",
}
}
]