1 /**
2  * General purpose utility functions for generating Dlang source files.
3  */
4 module openapi_client.util;
5 
6 import vibe.data.json : Json, serializeToJson;
7 import vibe.inet.webform : FormFields;
8 
9 import std.array : array, split, appender, Appender;
10 import std.algorithm : map, joiner;
11 import std.array : appender;
12 import std.conv : to;
13 import std.uni : toUpper, toLower, isUpper, isLower, isAlpha, isAlphaNum;
14 
15 /**
16  * Converts a long text into a a D-style comment block and writes it to a buffer.
17  */
18 void writeCommentBlock(
19     Appender!string buffer, string text, string prefix = "", size_t lineWidth = 100) {
20   size_t width = lineWidth - prefix.length - 3;
21   with (buffer) {
22     put(prefix ~ "/**\n");
23     foreach (string line; wordWrapText(text, width)) {
24       put(prefix ~ " * ");
25       put(line);
26       put("\n");
27     }
28     put(prefix ~ " */\n");
29   }
30 }
31 
32 /**
33  * Given a block of text, first split it using the "\n" escape, and then word-wrap each line.
34  */
35 string[] wordWrapText(string text, size_t lineWidth = 80, char sep = ' ') {
36   return text.split("\n").map!(line => wordWrapLine(line, lineWidth, sep)).joiner().array;
37 }
38 
39 /**
40  * Separates a single long line into separate lines, performing word-wrapping where possible.
41  *
42  * Params:
43  *   text = The long line of text to split into lines.
44  *   lineWidth = The maximum length of a line.
45  *   sep = The separator character that can be used for word-wrapping.
46  */
47 string[] wordWrapLine(string text, size_t lineWidth = 80, char sep = ' ') {
48   string[] results;
49   size_t start = 0;
50   while (start < text.length) {
51     size_t end;
52     if (start + lineWidth >= text.length) {
53       // There's not enough text for a full line, take what's there.
54       end = text.length;
55     } else {
56       end = start + lineWidth;
57       // Find the closest separator character to split the line.
58       while (text[end] != sep && end > start) {
59         end--;
60       }
61       // There may be no separator characters at all.
62       if (start == end) {
63         end = start + lineWidth;
64       }
65     }
66     results ~= text[start..end];
67     start = end;
68     // Consume any separators before the next line starts.
69     while (start < text.length && text[start] == sep) {
70       start++;
71     }
72   }
73   return results;
74 }
75 
76 unittest {
77   assert(
78       wordWrapText("aaaa aaaa aaaa aaaa bbbb bbbb bbbb bbbb", 20)
79       == ["aaaa aaaa aaaa aaaa", "bbbb bbbb bbbb bbbb"]);
80   assert(
81       wordWrapText("aaaa aaaa aaaa aaaaaa bbbb bbbb bbbb bbbb", 20)
82       == ["aaaa aaaa aaaa", "aaaaaa bbbb bbbb", "bbbb bbbb"]);
83   assert(
84       wordWrapText("aaaaaaaaaaaaaaaaaaaabbbbbbbbbb", 20)
85       == ["aaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbb"]);
86   assert(
87       wordWrapText("aaaaaaaaaaa\naaaaaaaaa\nbbbbbbbbbb", 20)
88       == ["aaaaaaaaaaa", "aaaaaaaaa", "bbbbbbbbbb"]);
89 }
90 
91 /// Converts a string in "snake_case" to "UpperCamelCase".
92 string toUpperCamelCase(string input) {
93   return toCamelCase(input, true);
94 }
95 
96 unittest {
97   assert(toUpperCamelCase("HamOnRye") == "HamOnRye");
98   assert(toUpperCamelCase("hamOnRye") == "HamOnRye");
99   assert(toUpperCamelCase("ham_On_Rye") == "HamOnRye");
100   assert(toUpperCamelCase("ham_on_rye") == "HamOnRye");
101   assert(toUpperCamelCase("ham.on.rye") == "HamOnRye");
102   assert(toUpperCamelCase("_ham_on_rye") == "HamOnRye");
103   assert(toUpperCamelCase("__ham__on__rye__") == "HamOnRye");
104   assert(toUpperCamelCase("3bird_ham") == "3BirdHam");
105 }
106 
107 /// Converts a string in "snake_case" to "lowerCamelCase".
108 string toLowerCamelCase(string input) {
109   return toCamelCase(input, false);
110 }
111 
112 unittest {
113   assert(toLowerCamelCase("HamOnRye") == "hamOnRye");
114   assert(toLowerCamelCase("hamOnRye") == "hamOnRye");
115   assert(toLowerCamelCase("ham_On_Rye") == "hamOnRye");
116   assert(toLowerCamelCase("ham_on_rye") == "hamOnRye");
117   assert(toLowerCamelCase("ham.on.rye") == "hamOnRye");
118   assert(toLowerCamelCase("_ham_on_rye") == "hamOnRye");
119   assert(toLowerCamelCase("__ham__on__rye__") == "hamOnRye");
120   assert(toLowerCamelCase("3bird_ham") == "3BirdHam");
121 }
122 
123 string toCamelCase(string input, bool firstCapital = true) {
124   auto str = appender!string();
125   size_t firstPos = 0;
126   while (!isAlphaNum(input[firstPos])) {
127     firstPos++;
128   }
129   if (firstCapital)
130     str.put(toUpper(input[firstPos]));
131   else
132     str.put(toLower(input[firstPos]));
133   bool newWord = !isAlpha(input[firstPos]);
134   foreach (c; input[firstPos + 1 .. $]) {
135     if (!isAlphaNum(c)) {
136       newWord = true;
137     } else if (!isAlpha(c)) {
138       newWord = true;
139       str.put(c);
140     } else if (newWord == true || isUpper(c)) {
141       str.put(toUpper(c));
142       newWord = false;
143     } else {
144       str.put(toLower(c));
145     }
146   }
147   return str[];
148 }
149 
150 /**
151  * Given a string with bracket parameters, e.g. "hello/{name}", and an associative array of
152  * parameters, substitute parameter values and return the new string.
153  *
154  * For example, `resolveTemplate("hello/{name}", ["name": "world"]}` would return "hello/world".
155  */
156 string resolveTemplate(string urlTemplate, string[string] params) {
157   Appender!string buf = appender!string();
158   Appender!string param = appender!string();
159   bool inParam = false;
160   foreach (char c; urlTemplate) {
161     if (!inParam) {
162       if (c == '{') {
163         inParam = true;
164         param = appender!string();
165       } else if (c == '}') {
166         throw new Exception("\"" ~ urlTemplate ~ "\": Unbalanced braces!");
167       } else {
168         buf.put(c);
169       }
170     } else {
171       if (c == '{') {
172         throw new Exception("\"" ~ urlTemplate ~ "\": Unbalanced braces!");
173       } else if (c == '}') {
174         inParam = false;
175         string* val = param[] in params;
176         if (val is null) {
177           throw new Exception("\"" ~ urlTemplate ~ "\": Missing value for parameter '"
178               ~ param[] ~ "'!");
179         }
180         buf.put(*val);
181       } else {
182         param.put(c);
183       }
184     }
185   }
186   return buf[];
187 }
188 
189 unittest {
190   import std.exception;
191 
192   assert(resolveTemplate("hello/{name}", ["name": "world"]) == "hello/world");
193   assert(resolveTemplate("{name}/{fish}/{name}", ["name": "world", "fish": "carp"])
194       == "world/carp/world");
195   assertThrown!Exception(resolveTemplate("abc{def{hij}", ["def": "ham"]));
196   assertThrown!Exception(resolveTemplate("ab}c{def}", ["def": "ham"]));
197 }
198 
199 /**
200  * Serialize an object according to DeepObject style.
201  *
202  * See_Also: https://swagger.io/docs/specification/serialization/
203  */
204 FormFields serializeDeepObject(T)(T obj) {
205   Json json = serializeToJson(obj);
206   FormFields fields;
207   serializeDeepObject(json, "", fields);
208   return fields;
209 }
210 
211 unittest {
212   import std.typecons : Nullable;
213   class Thing {
214     string f1;
215     Nullable!int f2;
216     static class InnerThing {
217       string f3;
218       Nullable!int f4;
219     }
220     InnerThing f5;
221   }
222   auto t = new Thing();
223   FormFields fields = serializeDeepObject(t);
224   assert(fields.length == 0, "fields = " ~ fields.toString);
225 }
226 
227 /// ditto
228 void serializeDeepObject(Json json, string keyPrefix, ref FormFields fields) {
229   if (json.type == Json.Type.array) {
230     foreach (size_t index, Json value; json.byIndexValue) {
231       serializeDeepObject(value, keyPrefix ~ "[" ~ index.to!string ~ "]", fields);
232     }
233   } else if (json.type == Json.Type.object) {
234     foreach (string key, Json value; json.byKeyValue ) {
235       serializeDeepObject(value, keyPrefix == "" ? key : keyPrefix ~ "[" ~ key ~ "]", fields);
236     }
237   } else if (
238       json.type != Json.Type.undefined
239       && json.type != Json.Type.null_
240       && !(json.type == Json.Type..string && json.get!string == "")) {
241     // Finally we have an actual value.
242     fields.addField(keyPrefix, json.to!string);
243   }
244 }
245 
246 /**
247  * A simple check whether an object is present, permitting consistency with [std.typecons.Nullable].
248  */
249 bool isNull(T)(const T obj) nothrow pure @nogc @safe {
250   return obj is null;
251 }
252 
253 /// ditto
254 bool isNull(const Json obj) nothrow @safe {
255   // A JSON object may exist will a null type, so we check for "undefined" instead.
256   return obj.type == Json.Type.undefined;
257 }