Martin Betak has uploaded a new change for review. Change subject: oVirt.js: Initial rewiev commit [WIP] Don't merge! ......................................................................
oVirt.js: Initial rewiev commit [WIP] Don't merge! Posting initial oVirt.js prototype by Vojtech, for easier review. Change-Id: Ic6af0fe3089d70816dc3e138fed66163af5ecb28 Signed-off-by: Martin Betak <mbe...@redhat.com> --- A oVirt.js 1 file changed, 360 insertions(+), 0 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/10/32310/1 diff --git a/oVirt.js b/oVirt.js new file mode 100644 index 0000000..7e84b07 --- /dev/null +++ b/oVirt.js @@ -0,0 +1,360 @@ +// oVirt.js prototype. +// Requires ECMAScript 6 compliant environment: +// http://www.ecma-international.org/ecma-262/5.1/ +// Currently requires Lo-Dash library: +// http://lodash.com/ + +// Contain the SDK within 'ovirt' global variable. +var ovirt = {}; + +// Expose services used by API implementation. +// Each service should be an object containing reusable methods. +// Services declared in this namespace have multiple roles: +// 1. existing services can be overridden to achieve SDK portability for given runtime environment +// 2. existing services can be used and new services can be added by SDK extensions or clients +ovirt.svc = (function () { + 'use strict'; + + var services = {}; + + // HTTP service for sending requests to remote server via HTTP protocol. + // This implementation uses XMLHttpRequest object supported by web browsers. + services.http = { + // Send HTTP request to remote server. + // 'httpParams' should be an object containing HTTP request parameters: + // - 'method': HTTP method to use, defaults to 'GET' + // - 'url': URL to send request to, defaults to empty string (document base URI) + // - 'headers': object containing extra HTTP headers, defaults to empty object + // - 'body': any value supported by XMLHttpRequest.send function, defaults to null + // 'onSuccess' should be a function to invoke when HTTP request completed successfully (status code 200). + // 'onError' should be a function to invoke when HTTP request failed (status code other than 200). + send(httpParams, onSuccess, onError) { + var xhr = new XMLHttpRequest(); + + // Sanitize inputs. + httpParams = httpParams || {}; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + var xhrSuccess = xhr.status >= 200 && xhr.status < 300; + + // Invoke appropriate handler function. + if (xhrSuccess && _.isFunction(onSuccess)) { + onSuccess(xhr.responseText); + } else if (!xhrSuccess && _.isFunction(onError)) { + onError(xhr.status); + } + } + }; + + // Resolve HTTP request parameters. + var method = httpParams.method || 'GET'; + var url = httpParams.url || ''; + var headers = httpParams.headers || {}; + var body = httpParams.body || null; + + // Initiate new request. + xhr.open(method, url, true); + + // Include credentials such as HTTP auth information and cookies in cross-site requests. + xhr.withCredentials = true; + + // Apply request headers. + Object.keys(headers).forEach(name => { + var value = headers[name] && headers[name].toString(); + xhr.setRequestHeader(name, value || ''); + }); + + // Fire off the request. + xhr.send(body); + } + }; + + return services; +})(); + +ovirt.api = (function (services, util) { + 'use strict'; + + var api = {}; + + class Operation { + constructor (spec) { + this.spec = spec; + this.transformResult = _getFnOrIdentity(spec.transformResult); + this.onSuccess = _.noop; + this.onError = _.noop; + } + + static get (spec) { + spec.httpMethod = 'GET'; + return new Operation(spec); + } + + static put (spec) { + spec.httpMethod = 'PUT'; + return new Operation(spec); + } + + static post (spec) { + spec.httpMethod = 'POST'; + return new Operation(spec); + } + + static delete (spec) { + spec.httpMethod = 'DELETE'; + return new Operation(spec); + } + + success(callback) { + this.onSuccess = _getFnOrNoop(callback); + return this; + } + + error() { + this.onError = _getFnOrNoop(callback); + return this; + } + + run() { + // Prepare request headers. + var headers = this.spec.httpHeaders || {}; + headers['Accept'] = 'application/json'; + headers['Content-Type'] = 'application/json'; + headers['Filter'] = api.options.filterResults; + + // Prepare request parameters. + var httpParams = { + method: this.spec.httpMethod, + url: api.options.engineBaseUrl + spec.apiContextPath, + headers, + body: spec.httpBody && JSON.stringify(spec.httpBody) + }; + + services.http.send(httpParams, + responseText => { + try { + // Parse response as JSON. + var result = JSON.parse(responseText); + // Transform resulting object, if necessary, + // and Invoke operation success callback. + this.onSuccess(this.transformResult(result)); + } catch (e) { + // TODO handle error properly + console.log('Error while parsing response: ' + e); + } + }, + errorCode => { + // TODO handle error properly + console.log('Request failed with status code ' + errorCode); + }); + } + + // If 'fn' is a function, return that function. + // Otherwise, return a no-op function. + static _getFnOrNoop (fn) { + return _.isFunction(fn) ? fn : _.noop; + } + + // If 'fn' is a function, return that function. + // Otherwise, return and identity function. + static _getFnOrIdentity (fn) { + return _.isFunction(fn) ? fn : _.identity; + } + } + + class Resource { + constructor(data) { + this.apiContextPath = data && data.href; + this._assignData(data); + + // Augment resource with nested resource collections. + var subCollections = data.subCollections || {}; + Object.keys(subCollections).forEach(name => { + this[name] = ResourceCollection.withContextPath(subCollections[name]); + }); + + // Augment resource with supported actions. + var actions = data.actions || {}; + Object.keys(actions).forEach(name => { + // Return operation to invoke given action using 'actionData' on this resource. + this[name] = function (actionData) { + return Operation.post({ + httpBody: actionData, + apiContextPath: actions[name] + }); + }; + }); + } + + static fromData (data) { + return new Resource(_transformResourceData(data)); + } + + update () { + return Operation.put({ + apiContextPath: this.apiContextPath, + httpBody: _computeDiff(this.originalData, this.data), + transformResult: data => { + this._assignData(data); + return data; + } + }); + } + + delete () { + return Operation.delete({ + apiContextPath: this.apiContextPath + }); + } + + _assignData (resourceData) { + var data = resourceData || {}; + this.originalData = _.cloneDeep(data); + + // Access resource data via getter/setter function with merge-on-set behavior. + // TODO prevent setting new properties (ones not already present in full data representation) + that.data = _makeGetSetMergeFn(that, data); + } + + // Transform raw resource data for use by resource objects. + static _transformResourceData (data) { + function definePropertyReadOnly (propName, propValue) { + Object.defineProperty(data, propName, { + value: propValue, + writable: false, + configurable: true, + enumerable: true + }); + } + + function reduceLinks (array) { + return array.reduce((result, link) => { + result[link.rel] = link.href; + return result; + }, {}); + } + + // Make 'id' non-writable. + definePropertyReadOnly('id', data.id); + + // Make 'href' non-writable. + definePropertyReadOnly('href', data.href); + + // Transform and expose nested resource collections. + var subCollectionLinks = data.link || []; + definePropertyReadOnly('subCollections', reduceLinks(subCollectionLinks)); + delete data.link; + + // Transform and expose supported actions. + var actionLinks = (data.actions && data.actions.link) || []; + definePropertyReadOnly('actions', reduceLinks(actionLinks)); + + return data; + } + + // Compute change between two objects and return a diff object containing the difference. + // The diff object will be null if there is no change. + static _computeDiff (original, modified) { + var diff = {}; + + if (_.isObject(original) && _.isObject(modified)) { + Object.keys(modified).forEach(function (key) { + var propertyDiff; + + if (!original.hasOwnProperty(key)) { + // Add new property missing in 'original' automatically into diff object. + diff[key] = modified[key]; + } else { + // Otherwise, compute change for existing property recursively and update diff object. + propertyDiff = computeDiff(original[key], modified[key]); + if (propertyDiff) { + diff[key] = propertyDiff; + } + } + }); + return _.isEqual(diff, {}) ? null : diff; + } else { + return _.isEqual(original, modified) ? null : modified; + } + } + + // Make a function acting as getter and setter on object value with merge-on-set behavior. + // Calling resulting function with an object argument will merge properties of that object into 'target' object and return 'that'. + // Otherwise, 'target' object will be returned. + static _makeGetSetMergeFn(that, target) { + return function (update) { + if (_.isObject(update)) { + _.merge(target, update); + return that; + } else { + return target; + } + }; + } + } + + class ResourceCollection { + constructor (apiContextPath) { + this.apiContextPath = apiContextPath; + } + + static withContextPath (apiContextPath) { + return new ResourceCollection(apiContextPath); + } + + get (id) { + return Operation.get({ + apiContextPath: `${this.apiContextPath}/${id}`, + transformResult: Resource.fromData + }); + } + + list () { + return Operation.get({ + apiContextPath: this.apiContextPath, + transformResult: _makeResourceArray + }); + } + + add (data) { + return Operation.post({ + apiContextPath: this.apiContextPath, + httpBody: data, + transformResult: Resource.fromData + }); + } + + delete (id) { + return Operation.delete({ + apiContextPath: `${this.apiContextPath}/${id}` + }) + } + + // Extract raw resource data from 'obj' and return array of resource objects. + static _makeResourceArray (obj) { + // Extract resource data, 'obj' should contain a single enumerable property. + var keys = Object.keys(obj); + var dataArray = (keys.length === 1) ? obj[keys[0]] : []; + + // Sanitize resource data. + dataArray = Array.isArray(dataArray) ? dataArray : []; + + // Return resource objects. + return dataArray.map(Resource.fromData); + } + } + + api = { + options: { + // Engine base URL, without a trailing slash. + engineBaseUrl: 'http://127.0.0.1:8080', + // Flag indicating whether to filter results based on user's permissions. + filterResults: false + } + }; + + api.datacenters = ResourceCollection.withContextPath('/ovirt-engine/api/datacenters'); + + return api; +})(ovirt.services); \ No newline at end of file -- To view, visit http://gerrit.ovirt.org/32310 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ic6af0fe3089d70816dc3e138fed66163af5ecb28 Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine Gerrit-Branch: master Gerrit-Owner: Martin Betak <mbe...@redhat.com> _______________________________________________ Engine-patches mailing list Engine-patches@ovirt.org http://lists.ovirt.org/mailman/listinfo/engine-patches