codenautas/json4all

View on GitHub
json4all.js

Summary

Maintainability
F
3 days
Test Coverage
"use strict";

(function codenautasModuleDefinition(root, name, factory) {
    /* global define */
    /* istanbul ignore next */
    if(typeof root.globalModuleName !== 'string'){
        root.globalModuleName = name;
    }
    /* istanbul ignore next */
    if(typeof exports === 'object' && typeof module === 'object'){
        module.exports = factory();
    }else if(typeof define === 'function' && define.amd){
        define(factory);
    }else if(typeof exports === 'object'){
        exports[root.globalModuleName] = factory();
    }else{
        root[root.globalModuleName] = factory();
    }
    root.globalModuleName = null;
})(/*jshint -W040 */this, 'json4all', function() {
/*jshint +W040 */

/*jshint -W004 */
var json4all = {};
/*jshint +W004 */

json4all.PretendedClass = Symbol("Pretended Class");

function functionName(fun) {
    /* istanbul ignore next */
    if('name' in fun){
        return fun.name;
    }else{
        return fun.toString().replace(/^\s*function\s*([^(]*)\((.|\s)*$/i,'$1');
    }
}

function constructorName(obj) {
    return functionName(obj[json4all.PretendedClass] || obj.constructor);
}

var root;

/* istanbul ignore next */
if(typeof window !== 'undefined'){
    root = window;
}else{
    root = global;
}

root.json4allRegisteredTypes = root.json4allRegisteredTypes || {};
var types = root.json4allRegisteredTypes;

var thisPlatformSkipsUndefinedInArrays = true;

JSON.stringify([undefined],function(key, value){
    if(key==='0'||key===0){
        thisPlatformSkipsUndefinedInArrays = false;
    }
    return value;
});


var thisPlatformHasReplacerBug = false;

/* istanbul ignore next */ // For IE compatibility
JSON.parse('{"3":"33"}',function(key, value){
    if(value===undefined){
        thisPlatformHasReplacerBug = true;
    }
    return value;
});

/* istanbul ignore next */
function InternalValueForUndefined(){ 
    /* istanbul ignore next */
    throw new Error('this is not a function'); 
}

/* istanbul ignore next */
function InternalValueForUnset(){ 
    /* istanbul ignore next */
    throw new Error('this is not a function'); 
}

json4all.directTypes={
    "Object"   : true,
    "Array"    : true,
    "anonymous": true,
    ""         : true
};

json4all.anonymizate = function(classedObject){
    var plainObject={};
    for(var attr in classedObject){
        plainObject[attr] = classedObject[attr];
    }
    return plainObject;
};

json4all.nonymizate = function(plainValue, Constructor){
    var typedValue = new Constructor();
    for(var attr in plainValue){
        typedValue[attr] = plainValue[attr];
    }
    return typedValue;
};

json4all.replacer = function replacer(key, value){
    var realValue = this[key];
    /* istanbul ignore next */ // For IE compatibility
    if(realValue===InternalValueForUndefined){
        this[key]=undefined;
        return {$special: "undefined"};
    }
    if(key!=='' && (!(key in this) || realValue!==null && realValue instanceof Function)){
        return {$special: "unset"};
    }
    if(realValue===undefined || typeof realValue === "undefined" || realValue!==null && realValue instanceof Function){
        return {$special: "undefined"};
    }
    if(realValue===null || typeof realValue!=='object' || key==='$escape'){
        return value;
    }
    if('$special' in realValue || '$escape' in realValue){
        return {$escape: realValue};
    }
    /* istanbul ignore next */ // For IE compatibility
    if(thisPlatformSkipsUndefinedInArrays && realValue instanceof Array){
        for(var i=0; i<realValue.length; i++){
            if(typeof realValue[i] === 'undefined'){
                realValue[i] = InternalValueForUndefined;
            }
        }
        return realValue;
    }
    var typeName = constructorName(realValue);
    if(json4all.directTypes[typeName]){
        return value;
    }else{
        var typeDef=types[typeName];
        if(typeDef){
            var replaced = {$special: typeDef.specialTag(realValue), $value: typeDef.deconstruct(realValue)};
            var ref = realValue[RefKey];
            if(ref!==undefined){
                replaced.$ref = ref;
                if(json4all.$RefStoreSpace == null){
                    delete replaced.$value;
                }
            }
            return replaced;
            // return {$special:'specialTag' in typeDef?typeDef.specialTag(realValue):typeName, $value: typeDef.deconstruct(realValue)};
        }else{
            console.log("JSON4all.stringify unregistered object type", typeName);
            throw new Error("JSON4all.stringify unregistered object type");
        }
    }
};

json4all.reviver = function reviver(key, plainValue){
    var log=false;
    var result;
    if(key==='$escape'){
        return plainValue;
    }else if(plainValue!==null && plainValue.$ref && json4all.$RefStoreSpace){
        var collection = json4all.$RefStoreSpace[plainValue.$ref[0][0]]
        if(!collection){
            throw new Error("json4all ref collection invalid");
        }
        var object = collection[plainValue.$ref[1]];
        if(!object){
            throw new Error("json4all ref key invalid");
        }
        return object;
    }else if(plainValue!==null && plainValue.$special){
        if(plainValue.$special==='undefined'){ 
            if(key===''){
                return undefined;
            }
            return InternalValueForUndefined;
        }else if(plainValue.$special==='unset'){
            return undefined;
        }else{
            var typeDef=types[plainValue.$special];
            if(typeDef){
                result = new typeDef.construct(plainValue.$value, typeDef.constructor);
            }else{
                console.log("JSON4all.parse invalid $special", plainValue.$special);
                throw new Error("JSON4all.parse invalid $special");
            }
        }
    }else if(plainValue!==null && plainValue.$escape){
        return plainValue.$escape;
    }else{
        result = plainValue;
        if(plainValue instanceof Object){
            for(var k in plainValue){
                if(plainValue[k]===InternalValueForUndefined){
                    plainValue[k]=undefined;
                }
            }
        }
    }
    if(plainValue !=null && plainValue.$ref){
        result[RefKey] = plainValue.$ref
    }
    return result;
};

var directRegExp = /^((null|true|false)$|[-\]\[{}".0-9])/;
var directStringRegExp  = /^[a-zA-ZÑñáéíóúüÁÉÍÓÚÜ_@-][0-9a-zA-ZÑñáéíóúüÁÉÍÓÚÜ_@-]{0,32}$/;
var AttributeNameRegExp = /^[a-zA-ZÑñáéíóúüÁÉÍÓÚÜ_@-][0-9a-zA-ZÑñáéíóúüÁÉÍÓÚÜ_@-]*($|,)/;

json4all.stringifyAnyPlace = function stringifyAnyPlace(value){
    return JSON.stringify(value, json4all.replacer);
};

var CHANNEL = {
    object:{separator: ','}, 
    array :{separator: ';'},
};

var SEPARATOR = {',':'object', ';':'array'}

function maxConsumer(){
    var fun = function(node){
        for (var channel in CHANNEL) {
            if (node[channel] > fun[channel]) fun[channel] = node[channel];
        }
        return node.value
    }
    for (var channel in CHANNEL) {
        fun[channel] = null;
    }
    return fun;
}

function simpleValue(value){
    return {value: value, object:0, array:0}
}

json4all.maxConsumer = maxConsumer;

json4all.mesureString = function mesureString(value){
    var currentSeparator = null;
    var currentLength = 0;
    var result = simpleValue(value);
    for (var char of value) {
        var channel = SEPARATOR[char];
        if (channel) {
            if (currentSeparator != char) {
                currentLength = 0
                currentSeparator = char
            }
            currentLength++
            if (currentLength > result[channel]) result[channel] = currentLength
        } else {
            currentSeparator = null;
        }
    }
    return result
}

json4all.quoteString = function quoteString(text, escapeColon){
    var value = directStringRegExp.test(text) && !directRegExp.test(text) ? text : JSON.stringify(text);
    if (escapeColon) value = value.replace(/:/g,'\\u003a');
    return json4all.mesureString(value);
}

var STAR = '*';
var BANG = '!';

json4all.toUrl = function toUrl(value){
    return json4all.toUrlConstruct(value).value;
}

var repeat = (str, count) => Array.prototype.concat(new Array(count +1)).join(str);

json4all.toUrlConstruct = function toUrlConstruct(value){
    if(value === null) return simpleValue("null");
    if(value === undefined) return simpleValue("!undefined");
    if(value instanceof Function) return simpleValue("!unset");
    var typeName = constructorName(value);
    if(typeof value === "string"){ 
        // if(value[0] === STAR){
        //     return json4all.mesureString(STAR + value);
        // }else{
            return json4all.quoteString(value);
        // }
    } else if (value && typeof value === "object" && !value[RefKey]) {
        if(!json4all.directTypes[typeName]){
            var typeDef=types[typeName];
            if(typeDef /* && (!onlyValues || typeDef.valueLike)*/){
                var serializedValue = typeDef.serialize(value)
                if (typeof serializedValue === "string") return json4all.mesureString(serializedValue);
                // serializedValue.value = BANG + serializedValue.value;
                return serializedValue;
            }else{
                console.log("JSON4all.stringify unregistered object type", typeName);
                throw new Error("JSON4all.stringify unregistered object type");
            }
        } else {
            var thisChannel = value instanceof Array ? 'array' : 'object';
            var parts = []
            var result = simpleValue(null);
            var cantSimplify = false;
            var max = maxConsumer();
            if (thisChannel == 'array') {
                for (var i = 0; i < value.length; i++) {
                    var pair = [max(json4all.toUrlConstruct(value[i]))]
                    parts.push(pair);
                }
            } else {
                for (var attr in value) {
                    var pair = [max(json4all.quoteString(attr, true)), max(json4all.toUrlConstruct(value[attr]))]
                    parts.push(pair);
                }
            }
            for (var channel in CHANNEL) {
                result[channel] = (max[channel] || 0) + (channel == thisChannel ? 1 : 0);
            }
            var PairSep = ':'
            var lengthSep = result[thisChannel];
            var ListSep = repeat(CHANNEL[thisChannel].separator, lengthSep);
            result.value = ListSep + parts.map(pair=>pair.join(PairSep)).join(ListSep)
            if (!cantSimplify && parts.length){
                return result;
            }
        }
    }
    return json4all.mesureString(JSON.stringify(value, json4all.replacer));
};

json4all.stringify = json4all.stringifyAnyPlace;

json4all.convertPlain2$special = function convertPlain2$special(o, internally){
    if(o!=null && o instanceof Object){
        for(var key in o){
            /* istanbul ignore next */
            var realKey = o[key]===undefined && !isNaN(key)?Number(key):key;
            json4all.convertPlain2$special(o[key], true);
            var newValue = json4all.reviver(key, o[key]); 
            if(newValue===undefined && !(o instanceof Array)){
                delete o[key];
            }else{
                o[key] = newValue;
            }
        }
    }
    if(!internally){
        return json4all.reviver('', o);
    }
};

json4all.isTesting = true;

json4all.getPlainObject = function getPlainObject(payload, separator, initial, PairSep){
    var i = 0;
    var ListSep = '';
    while (i < payload.length && payload[i] === separator) { i++; ListSep += separator }
    var result = initial
    var parts = payload.substr(i).split(ListSep);
    var index = -1;
    for (var part of parts) {
        if (PairSep) {
            var pos = part.indexOf(PairSep);
            if (pos < 0) {
                throw new Error('JSON4all.parse error. Lack of colon "' + PairSep + '" in object');
            }
            var value = part.substr(pos + PairSep.length);
            var index = json4all.parse(part.substr(0, pos));
        } else {
            var value = part;
            index ++;
        }
        var value = json4all.parse(value, true);
        if (value !== InternalValueForUnset) {
            result[index] = value
        }
    }
    return result;
}

json4all.parse = function parse(text, inner){
    var payload = text[0] === STAR ? text.substr(1) : text;
    if (payload[0] === BANG) {
        payload = payload.substr(1);
        if (payload === 'undefined') return undefined;
        if (payload === 'unset') {
            return inner ? InternalValueForUnset : undefined;
        }
        if (payload[0] == '@') {
            var commaPosition = payload.indexOf(',');
            var className = payload.substr(1, commaPosition - 1);
            var typeDef = types[className];
            if (!typeDef) {
                throw new Error('JSON4all.parse unrecognize className ' + className);
            }
            return typeDef.deserialize(payload).value;
        }
        if (payload[0] == '/') {
            var typeDef = types.RegExp;
            return typeDef.deserialize(payload).value;
        }
        throw new Error('JSON4all.parse unrecognize ! token')
    }
    if (payload[0] === CHANNEL.object.separator) {
        return json4all.getPlainObject(payload, CHANNEL.object.separator, {}, ':');
    }
    if (payload[0] === CHANNEL.array.separator) {
        return json4all.getPlainObject(payload, CHANNEL.array.separator, [], null);
    }
    for (var typeName in types) {
        var typeDef = types[typeName];
        var okDetected = null;
        /* istanbul ignore next */
        if(!typeDef.deserialize){
            throw new Error("JSON4all.parse type without deserialize: "+typeName)
        }
        var valueOk = null;
        var {ok, value} = typeDef.deserialize(payload);
        if(ok){
            valueOk = value;
            if(json4all.isTesting){
                if (okDetected) {
                    console.log("More than one way",typeName,okDetected,text,value,valueOk);
                    throw Error("")
                }
                okDetected = typeName;
            }
            break;
        }
    }
    if (okDetected) return valueOk;
    if (typeof text === "string" && !directRegExp.test(text)) {
        return text;
    }
    /* istanbul ignore next */ // For IE compatibility
    if(thisPlatformHasReplacerBug){
        var parsed=JSON.parse(text);
        return json4all.convertPlain2$special(parsed);
    } else {
        return JSON.parse(text, json4all.reviver);
    }
};

json4all.addType = function addType(typeConstructor, functions, skipIfExists){
    functions = functions || {};
    var constructorName = typeof typeConstructor === 'string'? typeConstructor: functionName(typeConstructor);
    if(skipIfExists && types[constructorName]){
        return;
    }
    var prefix = '@' + constructorName
    types[constructorName] = {
        construct: functions.construct || function construct(plainValue, constructor){
            return typeConstructor.JSON4reviver(plainValue, constructor);
        },
        deconstruct: functions.deconstruct || function deconstruct(o){
            return o.JSON4replacer();
        },
        specialTag: functions.specialTag || function specialTag(value){
            return constructorName;
        },
        serialize: functions.serialize || function serialize(o){
            var plainObject = 'JSON4replacer' in typeConstructor.prototype ? o.JSON4replacer() : functions.deconstruct(o);
            var result =  json4all.toUrlConstruct(plainObject)
            result.value = BANG + prefix + result.value;
            return result;
        },
        deserialize: functions.deserialize || function deserialize(plainValue){
            if (plainValue.startsWith(prefix + ',')) {
                var plainObject = json4all.parse(STAR + plainValue.substr(prefix.length),constructor);
                return {
                    ok:true, 
                    value:'JSON4replacer' in typeConstructor.prototype ? typeConstructor.JSON4reviver(plainObject, typeConstructor) : functions.construct(plainObject, typeConstructor)
                }
            }
            return {ok:false}
        },
        valueLike: functions.valueLike || typeConstructor.JSON4valueLike
    };
    if(typeof typeConstructor === 'function'){
        types[constructorName].constructor = typeConstructor;
    }
};

var NativeDateRegExp = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d*)?Z$/;

json4all.DateFunctions = {
    construct: function construct(plainValue){ 
        return new Date(plainValue); 
    }, 
    deconstruct: function deconstruct(o){
        return o.getTime();
    },
    serialize: function serialize(o){
        return o.toISOString();
    },
    deserialize: function deserialize(plainValue){
        var ok = NativeDateRegExp.test(plainValue);
        var value = ok && new Date(plainValue) || null;
        return {ok, value};
    },
    valueLike: true
}

json4all.addType(Date, json4all.DateFunctions, true);

json4all.addType(RegExp, {
    construct: function construct(plainValue){ 
        return new RegExp(plainValue.source, plainValue.flags); 
    }, 
    deconstruct: function deconstruct(o){
        return {source: o.source, flags: o.toString().substr(o.toString().lastIndexOf('/')+1)};
    },
    serialize: function serialize(o){
        return BANG + '/' + o.source + '/' + o.flags;
    },
    deserialize: function deserialize(plainValue){
        var ok = false
        var value = null;
        plainValue.replace(/^\/(.+)\/(\w*)$/, function(_, pattern, flags){
            value = new RegExp(pattern, flags)
            ok = true;
        });
        return {ok, value};
    },
    valueLike: true
});

json4all._types = types;

const RefKey = Symbol("RefKey for RefMap")

json4all.RefKey = RefKey;
// export class RefMap<K extends keyof object, V extends any> extends Map<K, V>{

json4all.RefStoreSpace = (myCollection)=>{
    json4all.$RefStoreSpace = myCollection;
}


/** 
 * @type {<K extends string|number|symbol, V extends {}> (path:(string|number|symbol)[]) => Partial<{[key in V]:v}} 
 * */
json4all.RefStore = (path)=>{
    var myCollection = json4all.$RefStoreSpace;
    /** @type {Partial<{[key in K]:V}>}  */
    var o = {}
    var p= new Proxy(o, {
        /*
        get:function(target, prop, _receiver) {
            return target[prop];
        },
        */
        /** @param {Partial<{[key in K]:V}>} target */
        /** @param {K} prop */
        /** @param {V} value */
        set:function(target, prop, value){
            if(
                // @ts-expect-error RefKey no está en el tipo original
                value[RefKey] == null 
            ){
                // @ts-expect-error RefKey no está en el tipo original
                value[RefKey] = [path, prop];
            }else if(
                // @ts-expect-error RefKey no está en el tipo original
                value[RefKey][1] != prop
            ){
                console.log("This value was stored in another place", path, prop)
                throw new Error("This value was stored in another place")
            }
            return Reflect.set(target, prop, value);
        }
    });
    myCollection[path[0]] = p;
    return p;
}

json4all.$props2serialize = Symbol("props2serialize")

json4all.addProperty = function(constructorPosition){
    var innerFunction = (classPrototype, prop, index)=>{
        if(!(json4all.$props2serialize in classPrototype)){
            classPrototype[json4all.$props2serialize] = []
        }
        var info={name:prop}
        if(typeof constructorPosition === "number") info.construct = constructorPosition
        classPrototype[json4all.$props2serialize].push(info)
    }
    if(typeof constructorPosition === "number"){
        return innerFunction
    }else{
        return innerFunction.apply(null, arguments);
    }
}

json4all.pretendClass = function(object, Constructor){
    object[json4all.PretendedClass] = Constructor
    // @ts-expect-error
    object.JSON4replacer = Constructor.prototype.JSON4replacer;
    // @ts-expect-error
    // object[JSON4all.$props2serialize] = Constructor.prototype[JSON4all.$props2serialize];
}

json4all.addClass = (constructor)=>{
    if(!(json4all.$props2serialize in constructor.prototype)){
        throw new Error("must add parameters or properties")
    }
    var propList = constructor.prototype[json4all.$props2serialize];
    constructor.prototype.JSON4replacer = function(){
        var o = {};
        for(var prop of propList){
            o[prop.name] = this[prop.name]
        }
        return o;
    };
    constructor.JSON4reviver = function(value, constructor){
        var constructParams = propList.filter(p=>p.construct!=null).map(p=>value[p.name])
        var o = new constructor(...constructParams);
        for(var p of propList){
            if(!p.construct){
                o[p.name] = value[p.name]
            }
        }
        return o;
    }
    json4all.addType(constructor)
}

return json4all;

});