Skip to content

Commit 9773208

Browse files
authored
Merge pull request #38 from common-workflow-language/mixin
Implement $mixin
2 parents 5d1bda4 + c3ada96 commit 9773208

File tree

4 files changed

+130
-15
lines changed

4 files changed

+130
-15
lines changed

schema_salad/metaschema/import_include.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,67 @@ This becomes:
110110
}
111111
}
112112
```
113+
114+
115+
## Mixin
116+
117+
During preprocessing traversal, an implementation must resolve `$mixin`
118+
directives. An `$mixin` directive is an object consisting of the field
119+
`$mixin` specifying resource by URI string. If there are additional fields in
120+
the `$mixin` object, these fields override fields in the object which is loaded
121+
from the `$mixin` URI.
122+
123+
The URI string must be resolved to an absolute URI using the link resolution
124+
rules described previously. Implementations must support loading from `file`,
125+
`http` and `https` resources. The URI referenced by `$mixin` must be loaded
126+
and recursively preprocessed as a Salad document. The external imported
127+
document must inherit the context of the importing document, however the file
128+
URI for processing the imported document must be the URI used to retrieve the
129+
imported document. The `$mixin` URI must not include a document fragment.
130+
131+
Once loaded and processed, the `$mixin` node is replaced in the document
132+
structure by the object or array yielded from the import operation.
133+
134+
URIs may reference document fragments which refer to specific an object in
135+
the target document. This indicates that the `$mixin` node must be
136+
replaced by only the object with the appropriate fragment identifier.
137+
138+
It is a fatal error if an import directive refers to an external resource
139+
or resource fragment which does not exist or is not accessible.
140+
141+
### Mixin example
142+
143+
mixin.yml:
144+
```
145+
{
146+
"hello": "world",
147+
"carrot": "orange"
148+
}
149+
150+
```
151+
152+
parent.yml:
153+
```
154+
{
155+
"form": {
156+
"bar": {
157+
"$mixin": "mixin.yml"
158+
"carrot": "cake"
159+
}
160+
}
161+
}
162+
163+
```
164+
165+
This becomes:
166+
167+
```
168+
{
169+
"form": {
170+
"bar": {
171+
"hello": "world",
172+
"carrot": "cake"
173+
}
174+
}
175+
}
176+
```

schema_salad/ref_resolver.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import requests
88
import urlparse
99
import re
10+
import copy
1011
import ruamel.yaml as yaml
1112
try:
1213
from ruamel.yaml import CSafeLoader as SafeLoader
@@ -259,25 +260,30 @@ def resolve_ref(self, ref, base_url=None, checklinks=True):
259260

260261
obj = None # type: Dict[unicode, Any]
261262
inc = False
263+
mixin = None
262264

263265
# If `ref` is a dict, look for special directives.
264266
if isinstance(ref, dict):
265267
obj = ref
266-
if u"$import" in ref:
268+
if u"$import" in obj:
267269
if len(obj) == 1:
268270
ref = obj[u"$import"]
269271
obj = None
270272
else:
271273
raise ValueError(
272-
"'$import' must be the only field in %s" % (str(obj)))
274+
u"'$import' must be the only field in %s" % (str(obj)))
273275
elif u"$include" in obj:
274276
if len(obj) == 1:
275277
ref = obj[u"$include"]
276278
inc = True
277279
obj = None
278280
else:
279281
raise ValueError(
280-
"'$include' must be the only field in %s" % (str(obj)))
282+
u"'$include' must be the only field in %s" % (str(obj)))
283+
elif u"$mixin" in obj:
284+
ref = obj[u"$mixin"]
285+
mixin = obj
286+
obj = None
281287
else:
282288
ref = None
283289
for identifier in self.identifiers:
@@ -286,15 +292,15 @@ def resolve_ref(self, ref, base_url=None, checklinks=True):
286292
break
287293
if not ref:
288294
raise ValueError(
289-
"Object `%s` does not have identifier field in %s" % (obj, self.identifiers))
295+
u"Object `%s` does not have identifier field in %s" % (obj, self.identifiers))
290296

291297
if not isinstance(ref, (str, unicode)):
292-
raise ValueError("Must be string: `%s`" % str(ref))
298+
raise ValueError(u"Must be string: `%s`" % str(ref))
293299

294300
url = self.expand_url(ref, base_url, scoped_id=(obj is not None))
295301

296302
# Has this reference been loaded already?
297-
if url in self.idx:
303+
if url in self.idx and (not mixin):
298304
return self.idx[url], {}
299305

300306
# "$include" directive means load raw text
@@ -309,14 +315,25 @@ def resolve_ref(self, ref, base_url=None, checklinks=True):
309315
else:
310316
# Load structured document
311317
doc_url, frg = urlparse.urldefrag(url)
312-
if doc_url in self.idx:
318+
if doc_url in self.idx and (not mixin):
319+
# If the base document is in the index, it was already loaded,
320+
# so if we didn't find the reference earlier then it must not
321+
# exist.
313322
raise validate.ValidationException(
314-
"Reference `#%s` not found in file `%s`." % (frg, doc_url))
315-
doc = self.fetch(doc_url)
323+
u"Reference `#%s` not found in file `%s`." % (frg, doc_url))
324+
doc = self.fetch(doc_url, inject_ids=(not mixin))
316325

317326
# Recursively expand urls and resolve directives
318-
resolved_obj, metadata = self.resolve_all(
319-
doc if doc else obj, doc_url, checklinks=checklinks)
327+
if mixin:
328+
doc = copy.deepcopy(doc)
329+
doc.update(mixin)
330+
del doc["$mixin"]
331+
url = None
332+
resolved_obj, metadata = self.resolve_all(
333+
doc, base_url, file_base=doc_url, checklinks=checklinks)
334+
else:
335+
resolved_obj, metadata = self.resolve_all(
336+
doc if doc else obj, doc_url, checklinks=checklinks)
320337

321338
# Requested reference should be in the index now, otherwise it's a bad
322339
# reference
@@ -477,6 +494,8 @@ def resolve_all(self, document, base_url, file_base=None, checklinks=True):
477494
# Handle $import and $include
478495
if (u'$import' in document or u'$include' in document):
479496
return self.resolve_ref(document, base_url=file_base, checklinks=checklinks)
497+
elif u'$mixin' in document:
498+
return self.resolve_ref(document, base_url=base_url, checklinks=checklinks)
480499
elif isinstance(document, list):
481500
pass
482501
else:
@@ -534,7 +553,7 @@ def resolve_all(self, document, base_url, file_base=None, checklinks=True):
534553
document[key], _ = loader.resolve_all(
535554
val, base_url, file_base=file_base, checklinks=False)
536555
except validate.ValidationException as v:
537-
_logger.debug("loader is %s", id(loader))
556+
_logger.warn("loader is %s", id(loader), exc_info=v)
538557
raise validate.ValidationException("(%s) (%s) Validation error in field %s:\n%s" % (
539558
id(loader), file_base, key, validate.indent(str(v))))
540559

@@ -543,7 +562,7 @@ def resolve_all(self, document, base_url, file_base=None, checklinks=True):
543562
try:
544563
while i < len(document):
545564
val = document[i]
546-
if isinstance(val, dict) and u"$import" in val:
565+
if isinstance(val, dict) and (u"$import" in val or u"$mixin" in val):
547566
l, _ = loader.resolve_ref(val, base_url=file_base, checklinks=False)
548567
if isinstance(l, list): # never true?
549568
del document[i]
@@ -558,6 +577,7 @@ def resolve_all(self, document, base_url, file_base=None, checklinks=True):
558577
val, base_url, file_base=file_base, checklinks=False)
559578
i += 1
560579
except validate.ValidationException as v:
580+
_logger.warn("failed", exc_info=v)
561581
raise validate.ValidationException("(%s) (%s) Validation error in position %i:\n%s" % (
562582
id(loader), file_base, i, validate.indent(str(v))))
563583

@@ -601,7 +621,7 @@ def fetch_text(self, url):
601621
else:
602622
raise ValueError('Unsupported scheme in url: %s' % url)
603623

604-
def fetch(self, url): # type: (unicode) -> Any
624+
def fetch(self, url, inject_ids=True): # type: (unicode, bool) -> Any
605625
if url in self.idx:
606626
return self.idx[url]
607627
try:
@@ -614,7 +634,7 @@ def fetch(self, url): # type: (unicode) -> Any
614634
result = yaml.load(textIO, Loader=SafeLoader)
615635
except yaml.parser.ParserError as e: # type: ignore
616636
raise validate.ValidationException("Syntax error %s" % (e))
617-
if isinstance(result, dict) and self.identifiers:
637+
if isinstance(result, dict) and inject_ids and self.identifiers:
618638
for identifier in self.identifiers:
619639
if identifier not in result:
620640
result[identifier] = url

tests/mixin.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
id: four
2+
one: two

tests/test_examples.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import rdflib
77
import ruamel.yaml as yaml
88
import json
9+
import os
910

1011
try:
1112
from ruamel.yaml import CSafeLoader as SafeLoader
@@ -322,5 +323,33 @@ def test_scoped_id(self):
322323
print(g.serialize(format="n3"))
323324

324325

326+
def test_mixin(self):
327+
ldr = schema_salad.ref_resolver.Loader({})
328+
ra = ldr.resolve_ref({"$mixin": "mixin.yml", "one": "five"},
329+
base_url="file://"+os.getcwd()+"/tests/")
330+
self.assertEqual({'id': 'four', 'one': 'five'}, ra[0])
331+
332+
ldr = schema_salad.ref_resolver.Loader({"id": "@id"})
333+
base_url="file://"+os.getcwd()+"/tests/"
334+
ra = ldr.resolve_all([{
335+
"id": "a",
336+
"m": {"$mixin": "mixin.yml"}
337+
}, {
338+
"id": "b",
339+
"m": {"$mixin": "mixin.yml"}
340+
}], base_url=base_url)
341+
self.assertEqual([{
342+
'id': base_url+'#a',
343+
'm': {
344+
'id': base_url+u'#a/four',
345+
'one': 'two'
346+
},
347+
}, {
348+
'id': base_url+'#b',
349+
'm': {
350+
'id': base_url+u'#b/four',
351+
'one': 'two'}
352+
}], ra[0])
353+
325354
if __name__ == '__main__':
326355
unittest.main()

0 commit comments

Comments
 (0)