Reference Source

js/base/introspectable.js


// NPM IMPORTS
import assert from 'assert'

// COMMON IMPORTS
import T from '../utils/types'


/**
 * Contextual constant for this file logs.
 * @private
 */
let context = 'common/base/introspectable'



/**
 * Base class to build easily classes.
 * @abstract
 * 
 * @author Luc BORIES
 * @license Apache-2.0
 * 
 * @example
* 	Property record format:
* 		{
*			name:string,
*			type:string,
*			value:any,
*			private:boolean,
*			getter:boolean,
* 			setter:boolean,
*			tester:boolean
* 		}
* 	API:
* 		->has_property(arg_name):boolean - test if instance has given named property.
* 		->add_property(arg_record:object):boolean - add given named property.
* 		
* 		->set_property_value(arg_name, arg_value)
* 		->set_properties_values(arg_values:object|array):boolean - set properties values with arg_values as { prop_name_1:value1, ... }
* 		
* 		->has_method(arg_name):boolean - test if instance has given named method.
* 		->add_method(arg_record:object):boolean - add given named method.
 */
export default class Introspectable
{
	/**
	 * Create an Introspectable instance.
	 * 
	 * @param {object} arg_properties - properties records as { name:'...', type:'', getter:true, setter: true, tester:true }.
	 * @param {object} arg_methods - methods records as { name:'...', type:'', functor:(...)=>{} }
	 * @param {object|undefined} arg_values - properties values (optional).
	 * 
	 * @returns {nothing}
	 */
	constructor(arg_properties=[], arg_methods=[], arg_values=undefined)
	{
		/**
		 * Class type flag.
		 * @type {boolean}
		 */
		this.is_introspectable = true
		
		/**
		 * Properties map.
		 * @type {object}
		 */
		this._properties = {}

		/**
		 * Properties names array.
		 * @type {array}
		 */
		this._properties_names = []

		/**
		 * Methods map.
		 * @type {object}
		 */
		this._methods = {}

		/**
		 * Methods names array.
		 * @type {array}
		 */
		this._methods_names = []

		// REGISTER PROPERTIES
		arg_properties.forEach(
			(prop_record)=>{
				this.add_property(prop_record)
			}
		)

		// REGISTER METHODS
		// arg_methods.forEach(
		// 	(method_record)=>{
				// this.add_method(method_record) // TODO
		// 	}
		// )

		// SET VALUES
		this.set_properties_values(arg_values)
	}



	/**
	 * Test if instance has given named property.
	 * 
	 * @param {string} arg_name - property name.
	 * 
	 * @returns {boolean}
	 */
	has_property(arg_name)
	{
		return (arg_name in this._properties) && ('current' in this[arg_name])
	}


	
	/**
	 * Test if instance has given named property.
	 * 
	 * @param {object} arg_record - property record.
	 * 
	 * @returns {boolean}
	 */
	add_property(arg_record)
	{
		assert( T.isObject(arg_record), context + ':add_property:bad property record object')

		const name     = T.isString(arg_record.name) ? arg_record.name.toLocaleLowerCase() : undefined
		const type     = T.isString(arg_record.type) ? arg_record.type.toLocaleLowerCase() : 'string'
		const value    = arg_record.value
		const is_priv  = T.isBoolean(arg_record.private) ? arg_record.private : true
		const getter   = T.isBoolean(arg_record.getter) ? arg_record.getter : true
		const setter   = T.isBoolean(arg_record.setter) ? arg_record.setter : true
		const tester   = T.isBoolean(arg_record.tester) ? arg_record.tester : true
		const serializable= T.isBoolean(arg_record.serializable) ? arg_record.serializable : true
		
		assert( T.isString(arg_record.name), context + ':add_property:bad property record.name string')

		const this_name = is_priv ? '_' + name : name
		assert( !(name in this) && !(this_name in this), context + ':add_property:property already exists')
		
		this._properties_names.push(name)
		this._properties[name] = {
			name:name,
			this_name:this_name,
			type:type,
			value:value,
			private:is_priv,
			serializable:serializable,
			getter:getter,
			setter:setter,
			tester:tester
		}

		// BUILD ATTRIBUTE
		/**
		 * Property value.
		 * @type {any}
		 */
		this[this_name] = value

		// BUILD GETTER
		if (getter)
		{
			/**
			 * Property getter.
			 * @type {function}
			 */
			this['get_' + name] = ()=>{
				return this[this_name]
			}
		}
		
		// BUILD SETTER
		if (setter)
		{
			/**
			 * Property setter.
			 * @type {function}
			 */
			this['set_' + name] = (arg_value)=>{
				this.set_property_value(name, arg_value, this_name)
			}
		}
		
		// BUILD TESTER
		if (tester)
		{
			/**
			 * Property tester.
			 * @type {function}
			 */
			this['has_' + name] = ()=>{
				return this[this_name] ? true : false
			}
		}

		return true
	}



	/**
	 * Set a property value.
	 * 
	 * @param {string} arg_name - property name.
	 * @param {any}    arg_value - property value.
	 * @param {string} arg_this_name - property attribute name.
	 * 
	 * @returns {boolean}
	 */
	set_property_value(arg_name, arg_value, arg_this_name)
	{
		const name = arg_this_name ? arg_this_name : arg_name

		/**
		 * Property value.
		 * @type {any}
		 */
		this[name] = arg_value
	}



	/**
	 * Get a property value.
	 * 
	 * @param {string} arg_name - property name.
	 * 
	 * @returns {any} - property value.
	 */
	get_property_value(arg_name)
	{
		if (! (arg_name in this._properties) )
		{
			return undefined
		}

		const property_record = this._properties[arg_name]
		const attr_name = property_record.this_name
		return this[attr_name]
	}



	/**
	 * Set properties values.
	 * 
	 * @param {object|array} arg_values - properties values.
	 * 
	 * @return {boolean}
	 */
	set_properties_values(arg_values)
	{
		if ( T.isArray(arg_values) || T.isArguments(arg_values) )
		{
			this._properties_names.forEach(
				(name, index)=>{
					const record = this._properties[name]
					const this_name = record.this_name
					const value = arg_values.length > index ? arg_values[index] : record.value
					this.set_property_value(name, value, this_name)
				}
			)

			return true
		}

		if ( T.isObject(arg_values) )
		{
			this._properties_names.forEach(
				(name)=>{
					const record = this._properties[name]
					const this_name = record.this_name
					const value = (name in arg_values) ? arg_values[name] : record.value
					this.set_property_value(name, value, this_name)
				}
			)

			return true
		}

		return false
	}



	/**
	 * Get properties values.
	 * 
	 * @return {object} - properties values.
	 */
	get_properties_values()
	{
		const values ={}
		this._properties_names.forEach(
			(name)=>{
				if ( this._properties[name].serializable )
				{
					values[name] = this.get_property_value(name)
				}
			}
		)
		return values
	}



	/**
	 * Test if instance has given named method.
	 * 
	 * @param {string} arg_name - method name.
	 * 
	 * @returns {boolean}
	 */
	has_method(arg_name)
	{
		return (arg_name in this._methods) && (arg_name in this) && T.isFunction(this[arg_name])
	}
}