Reference Source

js/utils/transform.js


// NPM IMPORTS
import assert from 'assert'
import Ramda from 'ramda'

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


const context = 'common/utils/transform'


const cursor_at_index = Ramda.lensIndex
// const cursor_at_key = Ramda.lensProp
const get = Ramda.view
const prop = Ramda.prop // f(field_name, value_to_query)
const prop_default = Ramda.propOr // f(default_value, field_name, value_to_query)
const deep_prop = Ramda.path // f(field_path, value_to_query)
// const clone = Ramda.clone
// const map = Ramda.map
const merge = Ramda.merge
const flatten = Ramda.flatten


/*
	FLAT
	
	Example:
		stream values: {
			target:'...',
			ts:'...',
			datas:[
				{level:'...', text:'...'},
				{level:'...', text:'...'},
				{level:'...', text:'...'}
			]
		}
	Attempded result: an array of {
			target:'...',
			timestamp:'...',
			level:'...',
			text:'...'
		}
	Transform object: {
		result_type:'array',
		flat_field_name:'datas'
		flat_fields:[
			{
				name:'level,
				flat_path:['datas'],
				path:['level']
			},
			{
				name:'text,
				flat_path:['datas'],
				path:['text']
			}
		],
		fields:[
			{
				name:'target,
				path:'target'
			},
			{
				name:'timestamp,
				path:'ts'
			}
		]
	}
*/



/**
 * Function to extract avalue from an object. 
 * @public
 * @param {object} arg_field_config - field configuration.
 * @returns {function} - transformation function : data_in (object) => datas_out (any)
 */
export const extract = (arg_field_config) => {
	// console.log(arg_field_config, context + ':extract:arg_field_config')

	assert( T.isObject(arg_field_config), context + ':flat:bad field object')
	
	const field_name = prop_default('unnamed field', 'name', arg_field_config)
	const field_path = prop_default(undefined, 'path', arg_field_config)
	const field_value = prop_default(undefined, 'value', arg_field_config)
	const flat_all = prop_default(undefined, 'flat_all', arg_field_config)

	// console.log(field_name, context + ':extract:field_name')
	// console.log(field_path, context + ':extract:field_path')
	// console.log(field_value, context + ':extract:field_value')
	
	const field_validate = prop_default(undefined, 'validate', arg_field_config)
	let default_value = undefined
	let validate_method = (f) => f
	
	// DEFINE VALIDATION
	if ( T.isObject(field_validate) )
	{
		// DEFAULT VALUE
		if ( ! T.isUndefined(field_validate.default) )
		{
			default_value = field_validate.default
		}
		
		// CHECK TYPE
		if ( T.isString(field_validate.type) && field_validate.type.length > 3 )
		{
			const check_method_name = 'is' + field_validate.type[0].toUpperLocaleString() + field_validate.type.slice(1).toLowerLocaleString()
			if ( T.isFunction(T[check_method_name]) )
			{
				const check_method = T.isFunction(T[check_method_name])
				validate_method = (f) => {
					return (x) => {
						const v = f(x)
						if ( check_method(v) )
						{
							return flat_all ? flatten(v) : v
						}
						return default_value
					}
				}
			}
		}
	}
	
	// XFORM = VALUE FROM NUMBER PATH
	if ( T.isNumber(field_path) )
	{
		const value_extractor = {
			name:field_name,
			extract:validate_method( get( cursor_at_index(field_path) ) )
		}
		return value_extractor
	}
	
	// XFORM = VALUE FROM STRING PATH
	if ( T.isString(field_path) )
	{
		// console.log('transform:value_extractor:value for field=%s path=%s', field_name, field_path)
		const value_extractor = {
			name:field_name,
			extract:validate_method( prop(field_path) )
		}
		return value_extractor
	}
	
	// XFORM = VALUE FROM ARRAY PATH
	if ( T.isArray(field_path) )
	{
		const value_extractor = {
			name:field_name,
			extract:validate_method( deep_prop(field_path) )
		}
		return value_extractor
	}
	
	// XFORM = CONSTANT
	if (field_value)
	{
		const value_extractor = {
			name:field_name,
			extract:() => validate_method( field_value )
		}
		return value_extractor
	}
	
	// RETURN PROPERTY WITH KEY = NAME
	const value_extractor = {
		name:field_name,
		extract:validate_method(
			(value)=>{
				// console.log('transform:value_extractor:value for field=%s:', field_name, value)
				if ( T.isObject(value) )
				{
					return prop_default(default_value, field_name)(value)
				}
				return T.isUndefined(value) ? default_value : value
			}
		)
	}
	
	return value_extractor
}



/**
 * Function to transform an object with an array attribute to a flat array. 
 * @public
 * @param {string} arg_array_name - array attribute name.
 * @param {array} arg_fields - unique fields to include into each output record.
 * @param {array} arg_flat_fields - fields of included array to pick into each output record.
 * @param {string} arg_results_type - output record type ('array' or 'object').
 * @returns {function} - transformation function : data_in (object) => datas_out (array of object|array)
 */
export const flat = (arg_array_name, arg_fields, arg_flat_fields, arg_results_type) => {
	assert( T.isString(arg_array_name), context + ':flat:bad array name')
	assert( T.isArray(arg_fields), context + ':flat:bad fields array')
	
	return (arg_value) => {
		if ( ! (arg_array_name in arg_value) )
		{
			return []
		}
		
		// INIT OUTPUT RESULTS
		let results = []
		
		// EXTRACT REPEATED VALUES
		let values_to_repeat_extractors = []
		arg_fields.forEach(
			(field) => {
				const field_extractor = extract(field)
				
				assert( T.isObject(field_extractor), context + ':flat:bad field extractor object')
				assert( T.isString(field_extractor.name), context + ':flat:bad field extractor name string')
				assert( T.isFunction(field_extractor.extract), context + ':flat:bad field extractor extract funcytion')
				
				values_to_repeat_extractors.push(field_extractor)
			}
		)
		let values_to_repeat = out(values_to_repeat_extractors, arg_results_type)(arg_value)
		
		// GET VALUES OF COLLECTION TO FLAT EXTRACTORS
		let flat_extractors = []
		arg_flat_fields.forEach(
			(field) => {
				const field_extractor = extract(field)
				
				assert( T.isObject(field_extractor), context + ':flat:bad flat field extractor object')
				assert( T.isString(field_extractor.name), context + ':flat:bad flat field extractor name string')
				assert( T.isFunction(field_extractor.extract), context + ':flat:bad flat field extractor extract funcytion')
				
				flat_extractors.push(field_extractor)
			}
		)
		let flat_extractors_out = out(flat_extractors, arg_results_type)
		
		// LOOP ON VALUES TO FLAT
		const array_to_flat = prop_default([], arg_array_name, arg_value)
		array_to_flat.forEach(
			(array_to_flat_item) => {
				let values_to_flat = flat_extractors_out(array_to_flat_item)
				
				const values = arg_results_type == 'object' ? merge(values_to_repeat, values_to_flat) : [].concat(values_to_repeat, values_to_flat)
				results.push(values)
			}
		)
		
		return results
	}
}



/**
 * Output
 * @public
 * @param {array} arg_extractors - fields extractors objects array with extractor:{ name:'...', extract:(any)=>(any) }).
 * @param {string} arg_results_type - output record type ('array' or 'object').
 * @returns {function} - transformation function : data_in (object|array) => datas_out (array or object)
 */
export const out = (arg_extractors, arg_results_type) => {
	const xform_array = (arg_stream_value) => {
		let result = []
		arg_extractors.forEach(
			(field_xform) => {
				result.push( field_xform.extract(arg_stream_value) )
			}
		)
		return result
	}
	
	const xform_object = (arg_stream_value) => {
		let result = {}
		arg_extractors.forEach(
			(field_xform) => {
				const field_name = field_xform.name
				result[field_name] = field_xform.extract(arg_stream_value)
			}
		)
		return result
	}
	
	
	let output_xformer = arg_stream_value => arg_stream_value
	if (arg_results_type == 'object')
	{
		output_xformer = xform_object
	}
	
	else if (arg_results_type == 'array')
	{
		output_xformer = xform_array
	}

	else if (arg_results_type == 'single' && arg_extractors.length == 1)
	{
		const field_xform = arg_extractors[0]
		output_xformer = (arg_stream_value) => {
			return field_xform.extract(arg_stream_value)
		}			
	}

	return output_xformer
}



/**
 * Function to transform a structured data to an other data regarding a transformation configuration. 
 * @public
 * @param {object} arg_xform - transformation definition.
 * @returns {function} - transformation function : data_in (object|array) => datas_out (array or object)
 */
export const transform = (arg_xform) => {
	assert( T.isObject(arg_xform), context + ':transform:bad xform object')
	
	const result_type = prop_default('array', 'result_type', arg_xform)
	const loop_on_keys = prop_default(undefined, 'loop_on_keys', arg_xform)
	const fields = prop_default([], 'fields', arg_xform)
	const flat_field_name = prop_default(undefined, 'flat_field_name', arg_xform)
	const flat_fields = prop_default(undefined, 'flat_fields', arg_xform)
	
	let extractors = []
	
	// WITH ARRAY TO FLAT
	if ( T.isString(flat_field_name) && T.isArray(flat_fields) )
	{
		return flat(flat_field_name,fields, flat_fields, result_type)
	}
	
	// WITHOUT ARRAY TO FLAT
	else
	{
		fields.forEach(
			(field) => {
				// console.log('tranform:WITHOUT ARRAY TO FLAT:fields.forEach', field)
				const value_extractor = extract(field)
				extractors.push(value_extractor)
			}
		)
	}
	
	// FORMAT OUTPUT
	const output_xformer = out(extractors, result_type)
	
	const output_extractor = (arg_value) => {

		if ( T.isArray(arg_value) )
		{
			let results = []
			arg_value.forEach(
				(value) => {
					const output_value = output_xformer(value)
					results.push(output_value)
				}
			)
			// console.log(context + ':output_extractor:isArray:results', results)
			return results
		}

		if ( T.isObject(arg_value) )
		{
			const output_value = output_xformer(arg_value)
			// console.log(context + ':output_extractor:isObject:output_value', output_value)
			return output_value
		}

		if (! loop_on_keys)
		{
			console.warn(context + ':output_extractor:not object/array:results', arg_value)
		}

		return undefined
	}
	
	if (loop_on_keys)
	{
		// console.log(context + ':loop_on_keys enabled')
		
		const loop_extractor = (arg_values) => {
			let results = []
			const keys = Object.keys(arg_values)
			
			// PROBLEM WITH NODEJS 0.10
			// for(let loop_key of keys)
			// {
			for(let loop_index = 0 ; loop_index < keys.length ; loop_index++)
			{
				const loop_key = keys[loop_index]
				// console.log(context + ':loop_on_keys:key', loop_key)
				const loop_value = arg_values[loop_key]
				const loop_extracted = output_extractor(loop_value)
				if (loop_extracted)
				{
					results.push(loop_extracted)
				}
			}
			
			return results
		}
		
		return loop_extractor
	}
	
	return output_extractor
}