Making: github.com/ChpShy/json… Address: experience chpshy. Making. IO/json2ts/ind…

introduce

After the project was slowly integrated into VUE3, there was a question of whether to define the TS type or just be happy as an AS developer. Go ahead, there are too many types to define and develop efficiently; No, a lot of the hints are missing, and it’s not easy to read code if it’s maintained later (all any). In a responsible manner, or want to add.

But what about the return of the back-end interface? They do have interface documentation, but some fields are too many to copy. Think of a tool that converts json from backend documents directly to TS. However, some of them rely too much on JSON format, such as key values that must be double quoted, and many cannot parse arrays. This makes it hard to convert non-standard back-end documents, requiring formatting checks and double quotes…

So I thought I’d develop my own. The overall idea is to convert JSON into AST through compilation principles, then trasForm into the desired format, and finally generate the code.

The project uses Vitest as a testing tool, which is really very useful

The characteristics of

  • supportnull,undefined,boolean,string,number,object,array
  • supportkeyValues with double quotes, single quotes, or no symbols
  • Objects can be removed
  • .

To better parse the data model of the back-end document, even the value without a comma after it!

Implementation scheme

parse

The first step is to parse the data model into an AST. First, the entry, start parsing {XXX}

function parseChildren(context: ParserContext) {
  const nodes: AstChildNode[] = [];
  while(! isEnd(context)) { advanceSpaces(context);const s = context.source;
    // A new line
    if (s[0= = ='{') {
      advanceBy(context, 1);
    } else if (s[0= = ='} ') {
      advanceBy(context, 1);
      advanceSpaces(context);
      return nodes;
    } else if (s[0= = ='/') {
        // Parse the comment, because there are inline comments and newline comments, we need to determine if the number of lines after parsing is the same as the last key: value data.
      if (s[1= = ='/') {
        const lastNode = nodes[nodes.length - 1];
        let lastLine = -1;
        if (lastNode) {
          lastLine = lastNode.loc.end.line;
        }
        const currLine = getCursor(context).line;
        nodes.push(parseComment(context, currLine === lastLine));
        advanceSpaces(context);
      } else {
        throw new Error('Wrong remarks')}}else {
      // Parse the key: value modelnodes.push(parseData(context)); }}return nodes;
}
Copy the code

{key: ‘key’, value: ‘value’, type: ‘string’}

function parseData(context: ParserContext, keyName? : string) {
  advanceSpaces(context);
  const start = getCursor(context);
  const key = keyName || parseKey(context);
  const { value, type } = parseValue(context);
  const loc = getLoc(context, start);
  advanceSpaces(context);
  if (context.source[0= = =', ') {
    advanceBy(context, 1);
    advanceSpaces(context);
  }
  return {key, value, type, loc: loc};
}

function parseKey(context: ParserContext) {
  const s = context.source[0];
  let match = [];
  // Key of type "XXX"
  if (s === '"') {
    match = /^"(.[^"]*)/i.exec(context.source);
  } else if (s === ` '`) {
    // Key of type 'XXX'
    match = /^'(.[^']*)/i.exec(context.source);
  } else {
    // XXX: type key
    match = /(.[^:]*)/i.exec(context.source);
    match[1] = match[1].trim();
  }
  // Remove the trailing "or" or:
  advanceBy(context, match[0].length + 1);
  advanceSpaces(context);
  // Remove the colon after "and '
  if (context.source[0= = =':') {
    advanceBy(context, 1);
    advanceSpaces(context);
  }
  return match[1];
}

function parseValue(context: ParserContext) {
  let value = null;
  let type = null;
  let code = context.source[0];
  if (/ / ^ [0-9].test(code)) {
   / / number type
    value = parseNumber(context);
    type = NUMBER_TYPE;
  } else if (code === '"' || code === '\' ') {
  / / sring type
    value = parseString(context);
    type = STRING_TYPE;
  } else if (code === '[') {
  // Array type
    advanceBy(context, 1);
    value = parseArray(context);
    type = ARRAY_TYPE;
  } else if (code === '{') {
  // Object type, using recursion to continue parsing
    value = parseChildren(context);
    type = OBJECT_TYPE;
  } else if (context.source.indexOf('null') = = =0) {
  / / null type
    value = parseNull(context);
    type = NULL_TYPE;
  } else if (context.source.indexOf('true') = = =0 || context.source.indexOf('false') = = =0) {
  // Boolean type
    value = parseBoolean(context);
    type = BOOLEAN_TYPE;
  } else if (context.source.indexOf('undefined') = = =0) {
  / / undefined type
    value = parseUndefined(context);
    type = UNDEFINED_TYPE;
  }
  return {
    value,
    type
  }
}
Copy the code

The different types of specific processing methods are not listed. If you are interested, you can go to the source code. Finally, the AST is generated in the following format.


{
  "a": 123."b": {
    "c": "123"
  },
  d: [1.2.3]
} 

=>

{
    "key": "root"."type": "Root"."value": [{"key": "a"."value": "123"."type": "number"."loc": {... }}, {"key": "b"."value": [{
            "key": "c"."value": "123"."type": "string"."loc": {... }}]."type": "Object"."loc": {... }}, {"key": "d"."value": [{
            "key": "$ARRAY_ITEM$"."value": "1"."type": "number"."loc": {... }}, {"key": "$ARRAY_ITEM$"."value": "2"."type": "number"."loc": {... }}, {"key": "$ARRAY_ITEM$"."value": "3"."type": "number"."loc": {... }}]."type": "Array"."loc": {... }}}]Copy the code

transform

In the end, generate needs to recursively combine each type into code, so transform mainly performs a layer of extraction and simple processing for type, such as the removal of repeated types in several groups.

Start by writing a traverser function that accesses the AST through the visitor mode. Visitor format: {string: {entry(node, parent){}, exit(node, parent){}}

function traverser(ast: AstChildNode, visiter: Visiter) {
  let root = visiter.Root;
  if (root) {
    root.entry && root.entry(ast, null);
  }
  traverseNode((ast.value as AstChildNode[]), ast, visiter);
  if (root) {
    root.exit && root.exit(ast, null);
  }
  return ast;
}
function traverseNode(nodes: AstChildNode[], parent: AstChildNode, visiter: Visiter) {
  nodes.forEach(node= > {
    let visit = visiter[node.type];
    if (visit) {
      visit.entry && visit.entry(node, parent);
    }
    if (isArray(node.value)) {
      traverseNode(node.value, node, visiter);
    }
    if(visit) { visit.exit && visit.exit(node, parent); }})}Copy the code

Then, different types are extracted and processed. Here, the object reference type feature is used to generate type tree typeValue, which is bound to the root node of ast:

function transform(ast: AstChildNode, options? : CompileOptions) {
  traverser(ast, {
    [STRING_TYPE]: {
      entry(node, parent) {
        if (node.key === ARRAY_ITEM) {
          parent.typeValue = parent.typeValue || [];
          (parent.typeValue as Array<string | Object>).push(node.type);
        } else {
          parent.typeValue = parent.typeValue || {};
          parent.typeValue[node.key] = node.type;
        }
      }
    },
    [OBJECT_TYPE]: {
      entry(node, parent) {
        if (node.key === ARRAY_ITEM) {
          parent.typeValue = parent.typeValue || [];
          node.typeValue = {};
          (parent.typeValue as Array<string | Object>).push(node.typeValue);
        } else {
          parent.typeValue = parent.typeValue || {};
          parent.typeValue[node.key] = node.typeValue = {};
        }
      }
    },
    [ARRAY_TYPE]: {
      entry(node, parent) {
        if (node.key === ARRAY_ITEM) {
          parent.typeValue = parent.typeValue || [];
          node.typeValue = [];
          (parent.typeValue as Array<string | Object>).push(node.typeValue);
        } else{ parent.typeValue = parent.typeValue || {}; parent.typeValue[node.key] = node.typeValue = []; }}},... });return ast;
}
Copy the code

Ast after transform is:

{
    key: "root".type: "Root".typeValue: {
      a: "number".b: { c: "string" },
      d: [ "number"."number"."number"]},value: [...]. }Copy the code

generate

Finally, the typeValue generates the final code:

function gen(typeValue: Record<string, string | Object> | Array<string | Object>) {
    let code = `{\n`;
    for (const key in typeValue) {
      const type = typeValue[key];
      code += this.genKey(key);
      if (isObject(type)) {
      // Process objects
        code += this.genObjcet(key, type);
      } else if (isArray(type)) {
      // Handle arrays
        code += this.options.parseArray ? this.genArray(key, type) : 'Array<any>';
      } else {
        code += type;
      }
      if (this.options.semicolon) {
        code += '; ';
      }
      code += '\n';
    }
    code += `}\n`;
    return code;
  }
  // Process objects
 function genObjcet(key:string, type: Record<string, string | Object>) {
    let code = ' ';
    / / recursive gen
    const objType = this.gen(type);
    if (this.options.spiltType) {
    // splitType is split into separate types. The separated types are placed in vars, binding only variable names to code, such as type NameType = {... }; type Result = { name: NameType }
      const varName = this.genName(key);
      this.vars += `type ${varName} = ${objType}; \n`;
      code += varName;
    } else {
      code += objType;
    }
    return code;
  }
// Handle arrays
 function genArray(key: string, types: Array<any>) {
    let code = `Array< `;
    // Use set to filter duplicate types
    const arrTypes = new Set(a); types.forEach(type= > {
      if (isArray(type)) {
      // Recursive array
        arrTypes.add(this.genArray(key, type));
      } if (isObject(type)) {
      // Recursive object
        arrTypes.add(this.genObjcet(key, type));
      } else {
      // Common typearrTypes.add(type); }}); code +=Array.from(arrTypes).join('|');
    code += '>';
    return code;
  }
Copy the code

The last

At present, it basically meets the common data transformation generation, but there are still many functions that need to be improved, some of which are listed in Github todolist. Md, welcome to improve together.

Feel free to leave your suggestions and thoughts in the comments section.