1 /**
2 This module implements ScriptObject, the base class for builtin Mildew objects.
3 
4 ────────────────────────────────────────────────────────────────────────────────
5 
6 Copyright (C) 2021 pillager86.rf.gd
7 
8 This program is free software: you can redistribute it and/or modify it under 
9 the terms of the GNU General Public License as published by the Free Software 
10 Foundation, either version 3 of the License, or (at your option) any later 
11 version.
12 
13 This program is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
15 PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License along with 
18 this program.  If not, see <https://www.gnu.org/licenses/>.
19 */
20 module mildew.types.object;
21 
22 /**
23  * General Object class. Similar to JavaScript, this class works as a dictionary but 
24  * the keys must be strings. Native D objects can be stored in any ScriptObject or derived 
25  * class by assigning it to its nativeObject field. This is also the base class for
26  * arrays, strings, and functions so that those script values can have dictionary entries
27  * assigned to them as well.
28  */
29 class ScriptObject
30 {
31     import mildew.types.any: ScriptAny;
32     import mildew.types.func: ScriptFunction;
33 public:
34     /**
35      * Constructs a new ScriptObject that can be stored inside ScriptValue.
36      * Params:
37      *  typename = This does not have to be set to a meaningful value but constructors (calling script functions
38      *             with the new keyword) set this value to the name of the function.
39      *  proto = The object's __proto__ property. If a value is not found inside the current object's table, a chain
40      *          of prototypes is searched until reaching a null prototype. If this parameter is null, the value is
41      *          set to Object.prototype
42      *  native = A ScriptObject can contain a native D object that can be accessed later. This is used for binding
43      *           D classes.
44      */
45     this(in string typename, ScriptObject proto, Object native = null)
46     {
47         import mildew.types.bindings: getObjectPrototype;
48         _name = typename;
49         if(proto !is null)
50             _prototype = proto;
51         else
52             _prototype = getObjectPrototype;
53         _nativeObject = native;
54     }
55 
56     /**
57      * Empty constructor that leaves prototype, and nativeObject as null.
58      */
59     this(in string typename)
60     {
61         _name = typename;
62     }
63 
64     /// name property
65     string name() const { return _name; }
66 
67 	/// getters property
68 	auto getters() { return _getters; }
69 	/// setters property
70 	auto setters() { return _setters; }
71 
72     /// prototype property
73     auto prototype() { return _prototype; }
74 
75     /// prototype property (setter)
76     auto prototype(ScriptObject proto) { return _prototype = proto; }
77 
78     /// This property provides direct access to the dictionary
79     auto dictionary() { return _dictionary; }
80 
81     /**
82      * Add a getter. Getters should be added to a constructor function's "prototype" field
83      */
84     void addGetterProperty(in string propName, ScriptFunction getter)
85     {
86         _getters[propName] = getter;
87     }
88 
89     /**
90      * Add a setter. Setters should be added to a constructor function's "prototype" field
91      */
92     void addSetterProperty(in string propName, ScriptFunction setter)
93     {
94         _setters[propName] = setter;
95     }
96 
97     /**
98      * Looks up a field through the prototype chain. Note that this does not call any getters because
99      * it is not possible to pass an Environment to opIndex.
100      */
101     ScriptAny lookupField(in string name)
102     {
103         if(name == "__proto__")
104             return ScriptAny(_prototype);
105         if(name == "__super__")
106         {
107             //the super non-constructor expression should translate to "this.__proto__.constructor.__proto__.prototype"
108             if(_prototype && _prototype["constructor"])
109             {
110                 // .__proto__.constructor
111                 auto protoCtor = _prototype["constructor"].toValue!ScriptObject;
112                 if(protoCtor && protoCtor._prototype)
113                 {
114                     return protoCtor._prototype["prototype"];
115                 }
116             }
117         }
118         if(name in _dictionary)
119             return _dictionary[name];
120         if(_prototype !is null)
121             return _prototype.lookupField(name);
122         return ScriptAny.UNDEFINED;
123     }
124 
125     /**
126      * Shorthand for lookupField.
127      */
128     ScriptAny opIndex(in string index)
129     {
130         return lookupField(index);
131     }
132 
133     /**
134      * Assigns a field to the current object. This does not call any setters.
135      */
136     ScriptAny assignField(in string name, ScriptAny value)
137     {
138         if(name == "__proto__")
139         {
140             _prototype = value.toValue!ScriptObject;
141         }
142         else if(name == "__super__")
143         {
144             return value; // this can't be assigned directly
145         }
146         else
147         {
148             _dictionary[name] = value;
149         }
150         return value;
151     }
152 
153     /**
154      * Determines if there is a getter for a given property
155      */
156     bool hasGetter(in string propName)
157     {
158         auto objectToSearch = this;
159         while(objectToSearch !is null)
160         {
161             if(propName in objectToSearch._getters)
162                 return true;
163             objectToSearch = objectToSearch._prototype;
164         }
165         return false;
166     }
167 
168     /**
169      * Determines if there is a setter for a given property
170      */
171     bool hasSetter(in string propName)
172     {
173         auto objectToSearch = this;
174         while(objectToSearch !is null)
175         {
176             if(propName in objectToSearch._setters)
177                 return true;
178             objectToSearch = objectToSearch._prototype;
179         }
180         return false;
181     }
182 
183     /**
184      * Find a getter in the prototype chain
185      */
186     ScriptFunction findGetter(in string propName)
187     {
188         auto objectToSearch = this;
189         while(objectToSearch !is null)
190         {
191             if(propName in objectToSearch._getters)
192                 return objectToSearch._getters[propName];
193             objectToSearch = objectToSearch._prototype;
194         }
195         return null;
196     }
197 
198     /**
199      * Find a setter in the prototype chain
200      */
201      ScriptFunction findSetter(in string propName)
202      {
203         auto objectToSearch = this;
204         while(objectToSearch !is null)
205         {
206             if(propName in objectToSearch._setters)
207                 return objectToSearch._setters[propName];
208             objectToSearch = objectToSearch._prototype;
209         }
210         return null;
211      }
212 
213     /**
214      * Shorthand for assignField
215      */
216     ScriptAny opIndexAssign(T)(T value, in string index)
217     {
218         static if(is(T==ScriptAny))
219             return assignField(index, value);
220         else
221         {
222             ScriptAny any = value;
223             return assignField(index, any);
224         }
225     }
226 
227     /**
228      * Returns a property descriptor without searching the prototype chain. The object returned is
229      * an object possibly containing get, set, or value fields.
230      */
231     ScriptObject getOwnPropertyOrFieldDescriptor(in string propName)
232     {
233         ScriptObject property = new ScriptObject("property", null);
234         // find the getter
235         auto objectToSearch = this;
236         if(propName in objectToSearch._getters)
237             property["get"] = objectToSearch._getters[propName];
238         if(propName in objectToSearch._setters)
239             property["set"] = objectToSearch._setters[propName];
240         if(propName in objectToSearch._dictionary)
241             property["value"] = _dictionary[propName];
242         objectToSearch = objectToSearch._prototype;
243         return property;
244     }
245 
246     ScriptObject getOwnFieldOrPropertyDescriptors()
247     {
248         auto property = new ScriptObject("descriptors", null);
249         foreach(k,v ; _dictionary)
250         {
251             auto descriptor = new ScriptObject("descriptor", null);
252             descriptor["value"] = v;
253             property[k] = descriptor;
254         }
255         foreach(k,v ; _getters)
256         {
257             auto descriptor = new ScriptObject("descriptor", null);
258             descriptor["get"] = v;
259             property[k] = descriptor;
260         }
261         foreach(k, v ; _setters)
262         {
263             auto descriptor = property[k].toValue!ScriptObject;
264             if(descriptor is null)
265                 descriptor = new ScriptObject("descriptor", null);
266             descriptor["set"] = v;
267             property[k] = descriptor;
268         }
269         return property;
270     }
271 
272     /**
273      * Tests whether or not a property or field exists in this object without searching the
274      * __proto__ chain.
275      */
276     bool hasOwnFieldOrProperty(in string propOrFieldName)
277     {
278         if(propOrFieldName in _dictionary)
279             return true;
280         if(propOrFieldName in _getters)
281             return true;
282         if(propOrFieldName in _setters)
283             return true;
284         return false;
285     }
286 
287     /**
288      * If a native object was stored inside this ScriptObject, it can be retrieved with this function.
289      * Note that one must always check that the return value isn't null because all functions can be
290      * called with invalid "this" objects using functionName.call.
291      */
292     T nativeObject(T)() const
293     {
294         static if(is(T == class) || is(T == interface))
295             return cast(T)_nativeObject;
296         else
297             static assert(false, "This method can only be used with D classes and interfaces");
298     }
299 
300     /**
301      * Native object can also be written because this is how binding works. Constructors
302      * receive a premade ScriptObject as the "this" with the name and prototype already set.
303      * Native D constructor functions have to set this property.
304      */
305     T nativeObject(T)(T obj)
306     {
307         static if(is(T == class) || is(T == interface))
308             return cast(T)(_nativeObject = obj);
309         else
310             static assert(false, "This method can only be used with D classes and interfaces");
311     }
312 
313     /**
314      * Returns a string with JSON like formatting representing the object's key-value pairs as well as
315      * any nested objects. In the future this will be replaced and an explicit function call will be
316      * required to print this detailed information.
317      */
318     override string toString() const
319     {
320         if(nativeObject!Object !is null)
321             return nativeObject!Object.toString();
322         return _name ~ " " ~ formattedString();
323     }
324 protected:
325 
326     /// The dictionary of key-value pairs
327     ScriptAny[string] _dictionary;
328 
329     /// The lookup table for getters
330     ScriptFunction[string] _getters;
331 
332     /// The lookup table for setters
333     ScriptFunction[string] _setters;
334 
335 private:
336 
337     // TODO complete rewrite
338     string formattedString(int indent = 0) const
339     {
340         immutable indentation = "    ";
341         auto result = "{";
342         size_t counter = 0;
343         immutable keyLength = _dictionary.keys.length;
344         foreach(k, v ; _dictionary)
345         {
346             for(int i = 0; i < indent; ++i)
347                 result ~= indentation;
348             result ~= k ~ ": ";
349             if(v.type == ScriptAny.Type.OBJECT)
350             {
351                 if(!v.isNull)
352                     result ~= v.toValue!ScriptObject().formattedString(indent+1);
353                 else
354                     result ~= "<null object>";
355             }
356             else if(v.type == ScriptAny.Type.STRING)
357             {
358                 result ~= "\"" ~ v.toString() ~ "\"";
359             }
360             else
361                 result ~= v.toString();
362             if(counter < keyLength - 1)
363                 result ~= ", ";
364             ++counter;
365         }
366         // for(int i = 0; i < indent; ++i)
367         //    result ~= indentation;
368         result ~= "}";
369         return result;
370     }
371 
372     /// type name (Function or whatever)
373     string _name;
374     /// it can also hold a native object
375     Object _nativeObject;
376     /// prototype 
377     ScriptObject _prototype = null;
378 }