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