1 /**
2  * Authors: Tomoya Tanjo
3  * Copyright: © 2021 Tomoya Tanjo
4  * License: Apache-2.0
5  */
6 module salad.resolver;
7 
8 import dyaml : Node, NodeType;
9 
10 import salad.context : LoadingContext;
11 
12 /**
13  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Link_resolution
14  */
15 auto resolveLink(string link, in LoadingContext context) nothrow pure @safe
16 {
17     import std.algorithm : canFind, endsWith, findSplitAfter, findSplitBefore, startsWith;
18 
19     auto pathPortionOf(string uri)
20     {
21         if (auto split = uri.findSplitBefore("#"))
22         {
23             return split[0];
24         }
25         else
26         {
27             return uri;
28         }
29     }
30 
31     if (link.startsWith("#"))
32     {
33         // rerlative fragment identifier
34         return pathPortionOf(context.baseURI)~link;
35     }
36     else if (auto split = link.findSplitBefore(":"))
37     {
38         import std.algorithm : canFind;
39 
40         if (auto ns = split[0] in context.namespaces)
41         {
42             // resolved with namespaces
43             return *ns ~ split[1][1..$];
44         }
45         else if (link.canFind("://"))
46         {
47             // assumption: an absolute URI contains "://"
48             return link;
49         }
50         else
51         {
52             // unresolved link
53             return link;
54         }
55     }
56     else
57     {
58         // path relative reference
59         string pathPortionOfRefURI = pathPortionOf(link);
60         string pathPortionOfBaseURI = pathPortionOf(context.baseURI);
61         if (pathPortionOfBaseURI.endsWith("/"))
62         {
63             return context.baseURI ~ pathPortionOfRefURI;
64         }
65         else
66         {
67             import std.path : buildPath, dirName;
68             return context.baseURI.dirName.buildPath(link);
69         }
70     }
71 }
72 
73 /**
74  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Link_resolution_example
75  */
76 unittest
77 {
78     auto context = LoadingContext(
79         "http://example.com/base",
80         [
81             "acid": "http://example.com/acid#",
82         ]
83     );
84     
85     enum zero = "http://example.com/base/zero";
86     assert(zero.resolveLink(context) == zero);
87 
88     assert("one".resolveLink(context) == "http://example.com/one");
89     assert("two".resolveLink(context) == "http://example.com/two");
90     assert("#three".resolveLink(context) == "http://example.com/base#three");
91     assert("four#five".resolveLink(context) == "http://example.com/four#five");
92     assert("acid:six".resolveLink(context) == "http://example.com/acid#six");
93 }
94 
95 /**
96  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Import
97  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Include
98  */
99 auto preprocess(in Node node, in LoadingContext context)
100 {
101     import salad.fetcher : fetchNode, fetchText;
102 
103     if (node.type != NodeType.mapping)
104     {
105         return node;
106     }
107     else if (auto link = "$import" in node)
108     {
109         auto resolved = resolveLink(link.as!string, context);
110         return resolved.fetchNode;
111     }
112     else if (auto link = "$include" in node)
113     {
114         auto resolved = resolveLink(link.as!string, context);
115         return Node(resolved.fetchText);
116     }
117     else
118     {
119         return node;
120     }
121 }
122 
123 /**
124  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Import_example
125  */
126 unittest
127 {
128     import dyaml : Loader;
129     import std.path : absolutePath;
130 
131     auto context = LoadingContext(
132         "file://"~"examples/import/parent.json".absolutePath,
133     );
134 
135     enum str = q"EOS
136         "bar": {
137             "$import": "import.json"
138         }
139 EOS";
140 
141     auto node = Loader.fromString(str).load;
142     auto processed = node["bar"].preprocess(context);
143     assert("hello" in processed);
144     assert(processed["hello"] == "world");
145 }
146 
147 /**
148  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Include_example
149  */
150 unittest
151 {
152     import dyaml : Loader;
153     import std.path : absolutePath;
154 
155     auto context = LoadingContext(
156         "file://"~"examples/include/parent.json".absolutePath,
157     );
158 
159     enum str = q"EOS
160         "bar": {
161             "$include": "include.txt"
162         }
163 EOS";
164 
165     auto node = Loader.fromString(str).load;
166     auto processed = node["bar"].preprocess(context);
167     assert(processed == "hello world");
168 }