1 //todo this should be "assets" 2 module glued.adhesives.bundles; 3 4 import std.algorithm; 5 import std.range; 6 7 import optional; 8 9 size_t depth(string path){ 10 return path.split("/").length; 11 } 12 13 /** 14 * Generalization of the idea of a read-only text file. 15 */ 16 interface Asset { 17 @property 18 string scheme(); 19 20 @property 21 string path(); 22 23 @property //todo uri? urn? consider differences, I think its urn 24 final string url(){ 25 return scheme~"://"~path; 26 } 27 28 //todo support binary assets 29 // import(binary file) works properly 30 // string s; cast(byte[]) s works as well 31 // byte[] b; cast(string) b too 32 @property 33 string content(); 34 35 //todo size 36 } 37 38 //todo this should be AssetBundle 39 interface Bundle { 40 Asset[] ls(); //fixme registrar performs ls lazily (with auto) while this requires array result 41 42 @property 43 string scheme(); 44 45 bool exists(string path); 46 47 Optional!Asset find(string path); 48 49 final Asset get(string path){ 50 auto result = find(path); 51 if (result.empty) 52 return null; 53 return result.front(); 54 } 55 56 final Optional!string findContent(string path){ 57 return find(path).map!(bf => bf.content).toOptional; 58 } 59 60 final string getContent(string path){ 61 return get(path).content; 62 } 63 } 64 65 class GluedAsset: Asset { 66 @property 67 string scheme(){ 68 return "glue"; 69 } 70 71 private string _path; 72 private string _content; 73 74 this(string p, string c){ 75 _path = p; 76 _content = c; 77 } 78 79 @property 80 string path(){ 81 return _path; 82 } 83 84 @property 85 string content(){ 86 return _content; 87 } 88 } 89 90 class GluedBundle(string modName): Bundle { 91 import std.path; 92 93 @property 94 string scheme(){ 95 return "glue"; 96 } 97 98 enum directoryName = modName.split(".")[0..$-1].join("/"); 99 100 mixin("import "~modName~";"); 101 alias def = BundleDefinition; 102 alias backend = def.bundledFiles; 103 104 bool exists(string path){ 105 return dirName(path) == directoryName && baseName(path) in backend; 106 } 107 108 Optional!Asset find(string path) { 109 if (!exists(path)) 110 return no!Asset; 111 //fixme I guess we could copy even less if file impl would also have Definition imported 112 return (cast(Asset) new GluedAsset(path, backend[baseName(path)])).some; 113 } 114 115 Asset[] ls() { 116 return backend.keys().map!(x => cast(Asset) find(directoryName~"/"~x).front()).array; 117 } 118 } 119 120 //todo silent assumption - file is in UTF-8/aligned with string, not wstring, etc 121 class FileAsset: Asset { 122 import std.file; 123 124 private string fullPath; 125 126 this(string path){ 127 assert(path.exists && path.isFile); 128 fullPath = path; 129 } 130 131 @property 132 string scheme(){ 133 return "file"; 134 } 135 136 @property 137 string path(){ 138 return fullPath; 139 } 140 141 @property 142 string content(){ 143 return readText(fullPath); 144 } 145 } 146 147 //todo fs based bundles require a lot of testing 148 class DirectoryBundle: Bundle { 149 import std.path; 150 import std.file; 151 152 private string dirPath; 153 154 this(string path){ 155 version(Windows) 156 { 157 path = path.replace("\\", "/"); 158 } 159 assert(path.exists && path.isDir); //todo exception 160 dirPath = path; 161 } 162 163 @property 164 string scheme(){ 165 return "file"; 166 } 167 168 bool exists(string path){ 169 return dirPath.chainPath(path).exists; 170 } 171 172 Optional!Asset find(string path){ 173 if (!exists(path)){ 174 return no!Asset; 175 } 176 //fixme what if between find() and asset.content file will be removed? maybe its a good idea to read it eagerly? 177 return (cast(Asset) new FileAsset(cast(string) dirPath.chainPath(path).array)).some; 178 } 179 180 Asset[] ls(){ 181 return dirEntries(dirPath, SpanMode.depth).map!(x => cast(Asset) find(cast(string) asRelativePath(x.name, dirPath).array).front()).array; 182 } 183 } 184 185 class BuildTimeAsset(string name): Asset { 186 @property 187 string scheme(){ 188 return "build"; 189 } 190 191 @property 192 string path(){ 193 return name; 194 } 195 196 @property 197 string content(){ 198 return import(name); 199 } 200 } 201 202 version(unittest) 203 { 204 enum buildTimeAssetNames = ["buildLog.conf", "build.conf", "app.conf", "buildLog.test.conf", "build.test.conf", "app.test.conf"]; 205 } 206 else 207 { 208 enum buildTimeAssetNames = ["buildLog.conf", "build.conf", "app.conf"]; 209 } 210 211 212 enum buildTimeAssetPresent(string p) = __traits(compiles, import(p)); 213 214 class BuildTimeBundle: Bundle 215 { 216 private Asset[string] predefinedAssets; 217 218 this() 219 { 220 static foreach(p; buildTimeAssetNames) 221 { 222 static if (buildTimeAssetPresent!p) 223 { 224 predefinedAssets[p] = new BuildTimeAsset!p; 225 } 226 } 227 } 228 229 Asset[] ls() 230 { 231 return predefinedAssets.values().array; 232 } 233 234 @property 235 string scheme() 236 { 237 return "build"; 238 } 239 240 bool exists(string path) 241 { 242 return (path in predefinedAssets) !is null; 243 } 244 245 Optional!Asset find(string path) 246 { 247 if (!exists(path)) 248 return no!Asset; 249 return predefinedAssets[path].some; 250 } 251 } 252 253 //todo would logging here be useful? 254 class BundleRegistrar { 255 private Bundle[] backend; 256 257 void register(Bundle bundle){ 258 backend ~= bundle; 259 } 260 261 void register(string modName)(){ 262 register(buildBundle!modName()); 263 } 264 265 private Bundle buildBundle(string modName)(){ 266 return new GluedBundle!modName; 267 } 268 269 private Optional!Bundle containing(string scheme, string path){ 270 Bundle[] containing; 271 foreach (b; backend){ 272 if (b.scheme == scheme && b.exists(path)){ 273 containing ~= b; 274 } 275 } 276 if (containing.empty) 277 return no!Bundle; 278 if (containing.length == 1) 279 return containing[0].some; 280 assert(false); //todo exception 281 } 282 283 bool exists(string scheme, string path){ 284 return !containing(scheme, path).empty; 285 } 286 287 Optional!Asset find(string scheme, string path){ 288 return containing(scheme, path).map!(b => b.get(path)).toOptional; 289 } 290 291 auto ls(){ 292 return backend.map!(x => x.ls()).joiner; 293 } 294 295 final Asset get(string scheme, string path){ 296 auto result = find(scheme, path); 297 if (result.empty) 298 return null; 299 return result.front(); 300 } 301 302 final Optional!string findContent(string scheme, string path){ 303 return find(scheme, path).map!(bf => bf.content).toOptional; 304 } 305 306 final string getContent(string scheme, string path){ 307 return get(scheme, path).content; 308 } 309 }