1 /**
2  * This module implements ScriptObject, the base class for builtin Mildew objects.
3  */
4 module mildew.types.object;
5 
6 /**
7  * General Object class. Unlike JavaScript, the __proto__ property only shows up when asked for. This allows
8  * allows objects to be used as dictionaries without extraneous values showing up in the for-of loop. Native D objects
9  * can be stored in any ScriptObject or derived class by assigning it to its nativeObject field.
10  */
11 class ScriptObject
12 {
13     import mildew.context: Context;
14     import mildew.types.any: ScriptAny;
15     import mildew.types.func: ScriptFunction;
16 public:
17     /**
18      * Constructs a new ScriptObject that can be stored inside ScriptValue.
19      * Params:
20      *  typename = This does not have to be set to a meaningful value but constructors (calling script functions
21      *             with the new keyword) set this value to the name of the function.
22      *  proto = The object's __proto__ property. If a value is not found inside the current object's table, a chain
23      *          of prototypes is searched until reaching a null prototype. If this parameter is null, the value is
24      *          set to Object.prototype
25      *  native = A ScriptObject can contain a native D object that can be accessed later. This is used for binding
26      *           D classes.
27      */
28     this(in string typename, ScriptObject proto, Object native = null)
29     {
30         import mildew.types.bindings: getObjectPrototype;
31         _name = typename;
32         if(proto !is null)
33             _prototype = proto;
34         else
35             _prototype = getObjectPrototype;
36         _nativeObject = native;
37     }
38 
39     /**
40      * Empty constructor that leaves prototype, and nativeObject as null.
41      */
42     this(in string typename)
43     {
44         _name = typename;
45     }
46 
47     /// name property
48     string name() const { return _name; }
49 
50 	/// getters property
51 	auto getters() { return _getters; }
52 	/// setters property
53 	auto setters() { return _setters; }
54 
55     /// prototype property
56     auto prototype() { return _prototype; }
57 
58     /// prototype property (setter)
59     auto prototype(ScriptObject proto) { return _prototype = proto; }
60 
61     /// This property provides direct access to the dictionary
62     auto dictionary() { return _dictionary; }
63 
64     /**
65      * Add a getter. Getters should be added to a constructor function's "prototype" field
66      */
67     void addGetterProperty(in string propName, ScriptFunction getter)
68     {
69         _getters[propName] = getter;
70     }
71 
72     /**
73      * Add a setter. Setters should be added to a constructor function's "prototype" field
74      */
75     void addSetterProperty(in string propName, ScriptFunction setter)
76     {
77         _setters[propName] = setter;
78     }
79 
80     /**
81      * Looks up a field through the prototype chain. Note that this does not call any getters because
82      * it is not possible to pass a Context to opIndex.
83      */
84     ScriptAny lookupField(in string name)
85     {
86         if(name == "__proto__")
87             return ScriptAny(_prototype);
88         if(name in _dictionary)
89             return _dictionary[name];
90         if(_prototype !is null)
91             return _prototype.lookupField(name);
92         return ScriptAny.UNDEFINED;
93     }
94 
95     /**
96      * Shorthand for lookupField.
97      */
98     ScriptAny opIndex(in string index)
99     {
100         return lookupField(index);
101     }
102 
103     /**
104      * Assigns a field to the current object. This does not call any setters.
105      */
106     ScriptAny assignField(in string name, ScriptAny value)
107     {
108         if(name == "__proto__")
109         {
110             _prototype = value.toValue!ScriptObject;
111         }
112         else
113         {
114             _dictionary[name] = value;
115         }
116         return value;
117     }
118 
119     /**
120      * Determines if there is a getter for a given property
121      */
122     bool hasGetter(in string propName)
123     {
124         auto objectToSearch = this;
125         while(objectToSearch !is null)
126         {
127             if(propName in objectToSearch._getters)
128                 return true;
129             objectToSearch = objectToSearch._prototype;
130         }
131         return false;
132     }
133 
134     /**
135      * Determines if there is a setter for a given property
136      */
137     bool hasSetter(in string propName)
138     {
139         auto objectToSearch = this;
140         while(objectToSearch !is null)
141         {
142             if(propName in objectToSearch._setters)
143                 return true;
144             objectToSearch = objectToSearch._prototype;
145         }
146         return false;
147     }
148 
149     /**
150      * Shorthand for assignField
151      */
152     ScriptAny opIndexAssign(T)(T value, in string index)
153     {
154         static if(is(T==ScriptAny))
155             return assignField(index, value);
156         else
157         {
158             ScriptAny any = value;
159             return assignField(index, any);
160         }
161     }
162 
163     ScriptObject getOwnPropertyDescriptor(in string propName)
164     {
165         ScriptObject property = new ScriptObject("property", null);
166         // find the getter
167         auto objectToSearch = this;
168         if(propName in objectToSearch._getters)
169             property["get"] = objectToSearch._getters[propName];
170         if(propName in objectToSearch._setters)
171             property["set"] = objectToSearch._setters[propName];
172         objectToSearch = objectToSearch._prototype;
173         return property;
174     }
175 
176     /**
177      * If a native object was stored inside this ScriptObject, it can be retrieved with this function.
178      * Note that one must always check that the return value isn't null because all functions can be
179      * called with invalid "this" objects using functionName.call.
180      */
181     T nativeObject(T)() const
182     {
183         static if(is(T == class) || is(T == interface))
184             return cast(T)_nativeObject;
185         else
186             static assert(false, "This method can only be used with D classes and interfaces");
187     }
188 
189     /**
190      * Native object can also be written in case of inheritance by script
191      */
192     T nativeObject(T)(T obj)
193     {
194         static if(is(T == class) || is(T == interface))
195             return cast(T)(_nativeObject = obj);
196         else
197             static assert(false, "This method can only be used with D classes and interfaces");
198     }
199 
200     /**
201      * Returns a string with JSON like formatting representing the object's key-value pairs as well as
202      * any nested objects. In the future this will be replaced and an explicit function call will be
203      * required to print this detailed information.
204      */
205     override string toString() const
206     {
207         if(nativeObject!Object !is null)
208             return nativeObject!Object.toString();
209         return _name ~ " " ~ formattedString();
210     }
211 protected:
212 
213     /// The dictionary of key-value pairs
214     ScriptAny[string] _dictionary;
215 
216     /// The lookup table for getters
217     ScriptFunction[string] _getters;
218 
219     /// The lookup table for setters
220     ScriptFunction[string] _setters;
221 
222 private:
223 
224     string formattedString(int indent = 0) const
225     {
226         immutable indentation = "    ";
227         auto result = "{";
228         foreach(k, v ; _dictionary)
229         {
230             for(int i = 0; i < indent; ++i)
231                 result ~= indentation;
232             result ~= k ~ ": ";
233             if(v.type == ScriptAny.Type.OBJECT)
234             {
235                 if(!v.isNull)
236                     result ~= v.toValue!ScriptObject().formattedString(indent+1);
237                 else
238                     result ~= "<null object>";
239             }
240             else
241                 result ~= v.toString();
242             result ~= "\n";
243         }
244         for(int i = 0; i < indent; ++i)
245             result ~= indentation;
246         result ~= "}";
247         return result;
248     }
249 
250     /// type name (Function or whatever)
251     string _name;
252     /// it can also hold a native object
253     Object _nativeObject;
254     /// prototype 
255     ScriptObject _prototype = null;
256 }