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 }