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 }