LP1857351 (follow-up): Fix typo that led to failing test
[Evergreen.git] / Open-ILS / src / eg2 / src / app / core / idl.service.ts
1 import {Injectable} from '@angular/core';
2
3 // Added globally by /IDL2js
4 declare var _preload_fieldmapper_IDL: Object;
5
6 /**
7  * Every IDL object class implements this interface.
8  */
9 export interface IdlObject {
10     a: any[];
11     classname: string;
12     _isfieldmapper: boolean;
13     // Dynamically appended functions from the IDL.
14     [fields: string]: any;
15 }
16
17 @Injectable({providedIn: 'root'})
18 export class IdlService {
19
20     classes: any = {}; // IDL class metadata
21     constructors = {}; // IDL instance generators
22
23     /**
24      * Create a new IDL object instance.
25      */
26     create(cls: string, seed?: any[]): IdlObject {
27         if (this.constructors[cls]) {
28             return new this.constructors[cls](seed);
29         }
30         throw new Error(`No such IDL class ${cls}`);
31     }
32
33     parseIdl(): void {
34
35         try {
36             this.classes = _preload_fieldmapper_IDL;
37         } catch (E) {
38             console.error('IDL (IDL2js) not found.  Is the system running?');
39             return;
40         }
41
42         /**
43          * Creates the class constructor and getter/setter
44          * methods for each IDL class.
45          */
46         const mkclass = (cls, fields) => {
47             this.classes[cls].classname = cls;
48
49             // This dance lets us encode each IDL object with the
50             // IdlObject interface.  Useful for adding type restrictions
51             // where desired for functions, etc.
52             const generator: any = ((): IdlObject => {
53
54                 const x: any = function(seed) {
55                     this.a = seed || [];
56                     this.classname = cls;
57                     this._isfieldmapper = true;
58                 };
59
60                 fields.forEach(function(field, idx) {
61                     x.prototype[field.name] = function(n) {
62                         if (arguments.length === 1) {
63                             this.a[idx] = n;
64                         }
65                         return this.a[idx];
66                     };
67
68                     if (!field.label) {
69                         field.label = field.name;
70                     }
71
72                     // Coerce 'aou' links to datatype org_unit for consistency.
73                     if (field.datatype === 'link' && field.class === 'aou') {
74                         field.datatype = 'org_unit';
75                     }
76                 });
77
78                 return x;
79             });
80
81             this.constructors[cls] = generator();
82
83             // global class constructors required for JSON_v1.js
84             // TODO: polluting the window namespace w/ every IDL class
85             // is less than ideal.
86             window[cls] = this.constructors[cls];
87         };
88
89         Object.keys(this.classes).forEach(class_ => {
90             mkclass(class_, this.classes[class_].fields);
91         });
92     }
93
94     // Makes a deep copy of an IdlObject's / structures containing
95     // IdlObject's.  Note we don't use JSON cross-walk because our
96     // JSON lib does not handle circular references.
97     // @depth specifies the maximum number of steps through IdlObject'
98     // we will traverse.
99     clone(source: any, depth?: number): any {
100         if (depth === undefined) {
101             depth = 100;
102         }
103
104         let result;
105         if (typeof source === 'undefined' || source === null) {
106             return source;
107
108         } else if (source._isfieldmapper) {
109             // same depth because we're still cloning this same object
110             result = this.create(source.classname, this.clone(source.a, depth));
111
112         } else {
113             if (Array.isArray(source)) {
114                 result = [];
115             } else if (typeof source === 'object') { // source is not null
116                 result = {};
117             } else {
118                 return source; // primitive
119             }
120
121             for (const j in source) {
122                 if (source[j] === null || typeof source[j] === 'undefined') {
123                     result[j] = source[j];
124                 } else if (source[j]._isfieldmapper) {
125                     if (depth) {
126                         result[j] = this.clone(source[j], depth - 1);
127                     }
128                 } else {
129                     result[j] = this.clone(source[j], depth);
130                 }
131             }
132         }
133
134         return result;
135     }
136
137     // Given a field on an IDL class, returns the name of the field
138     // on the linked class that acts as the selector for the linked class.
139     // Returns null if no selector is found or the field is not a link.
140     getLinkSelector(fmClass: string, field: string): string {
141         let fieldDef = this.classes[fmClass].field_map[field];
142
143         if (!fieldDef) {
144             console.warn(
145                 `No such field "${field}" for IDL class "${fmClass}"`);
146             return null;
147         }
148
149         if (fieldDef.map) {
150             // For mapped fields, we want the selector field on the
151             // remotely linked object instead of the directly
152             // linked object.
153             const linkedClass = this.classes[fieldDef.class];
154             fieldDef = linkedClass.field_map[fieldDef.map];
155         }
156
157         if (fieldDef.class) {
158             return this.getClassSelector(fieldDef.class);
159         }
160         return null;
161     }
162
163     // Return the selector field for the class.  If no selector is
164     // defined, use 'name' if it exists as a field on the class.
165     getClassSelector(idlClass: string): string {
166
167         if (idlClass) {
168             const classDef = this.classes[idlClass];
169
170             if (classDef.pkey) {
171                 const selector = classDef.field_map[classDef.pkey].selector;
172                 if (selector) { return selector; }
173
174                 // No selector defined in the IDL, try 'name'.
175                 if ('name' in classDef.field_map) { return 'name'; }
176             }
177         }
178
179         return null;
180     }
181
182     toHash(obj: any, flatten?: boolean): any {
183
184         if (typeof obj !== 'object' || obj === null) {
185             return obj;
186         }
187
188         if (Array.isArray(obj)) {
189             return obj.map(item => this.toHash(item));
190         }
191
192         const fieldNames = obj._isfieldmapper ?
193             Object.keys(this.classes[obj.classname].field_map) :
194             Object.keys(obj);
195
196         const hash: any = {};
197         fieldNames.forEach(field => {
198
199             const val = this.toHash(
200                 typeof obj[field] === 'function' ?  obj[field]() : obj[field],
201                 flatten
202             );
203
204             if (val === undefined) { return; }
205
206             if (flatten && val !== null &&
207                 typeof val === 'object' && !Array.isArray(val)) {
208
209                 Object.keys(val).forEach(key => {
210                     const fname = field + '.' + key;
211                     hash[fname] = val[key];
212                 });
213
214             } else {
215                 hash[field] = val;
216             }
217         });
218
219         return hash;
220     }
221
222     // Returns true if both objects have the same IDL class and pkey value.
223     pkeyMatches(obj1: IdlObject, obj2: IdlObject) {
224         if (!obj1 || !obj2) { return false; }
225         const idlClass = obj1.classname;
226         if (idlClass !== obj2.classname) { return false; }
227         const pkeyField = this.classes[idlClass].pkey || 'id';
228         return obj1[pkeyField]() === obj2[pkeyField]();
229     }
230
231     // Sort an array of fields from the IDL (like you might get from calling
232     // this.idlClasses[classname][fields])
233
234     sortIdlFields(fields: any[], desiredOrder: string[]): any[] {
235         let newList = [];
236
237         desiredOrder.forEach(name => {
238             const match = fields.filter(field => field.name === name)[0];
239             if (match) { newList.push(match); }
240         });
241
242         // Sort remaining fields by label
243         const remainder = fields.filter(f => !desiredOrder.includes(f.name));
244         remainder.sort((a, b) => {
245             if (a.label && b.label) {
246                 return (a.label < b.label) ? -1 : 1;
247             } else if (a.label) {
248                 return -1;
249             } else if (b.label) {
250                 return 1;
251             }
252
253             // If no order specified and no labels to sort by,
254             // default to sorting by field name
255             return (a.name < b.name) ? -1 : 1;
256         });
257         newList = newList.concat(remainder);
258         return newList;
259     }
260 }
261