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 }