使用通用键重新构造对象/数组

3

我正在构建一个工具,允许用户从通用源(具有通用结构)中获取数据并重新映射属性以匹配我的API。

  1. 用户将输入源URL(REST API或JSON);
  2. 用户将输入基于JSON的DTO指令

DTO将是像这样的JSON:

{
  "data.collection.name": "section.title",
  "just.an.example": "data.foo",
}

当来源提供了具备此结构的数据时:

{
  "data": {
    "foo": "bar",
    "collection": {
      "totalCount": 123,
      "title": "Best sellers",
    }
  }
}

该功能将重新映射该值为:

{
  "section": {
     "title": "Best sellers"
  },
  "just": {
    "an": {
      "example": "bar"
    }
  }
}

我可以使用lodash中的getset方法来实现此操作,具体算法如下:

const data = {
  foo: "bar",
  collection: {
    title: "BestSellers",
  },
  just: {
    an: {
      example: "bar",
    },
  },
  nodes: {
    edges: [
      {
        title: "Orange juice",
        slug: "orange-juice",
      },
      {
        title: "Apple juice",
        slug: "apple-juice",
      },
    ],
  },
};

let transformed = {};

const transformer = {
  "collection.title": "section.title",
  "just.an.example": "foo",
};

Object.keys(transformer).forEach((key) => {
  const value = _.get(data, key);
  const path = transformer[key];

  _.set(transformed, path, value);
});

console.log(transformed);

现在我需要使用数组属性(如nodes.edges)来实现相同的结果。

User DTO JSON应该长成这个样子(不是必需的,但理想情况下):

{
  "data.collection.name": "section.title",
  "nodes.edges.title": "products.title",
  "nodes.edges.slug": "products.seo.slug",
}

将生成以下输出:

{
  "section": {
    "title": "BestSellers"
  },
  "products": [
    {
      "title": "Orange Juice",
      "seo": {
         "slug": "orange-juice"
      }
    },
    {
      "title": "Apple Juice",
      "seo": {
         "slug": "apple-juice"
      }
    }
  ]
}

你可以在DTO定义中添加node.edges[].title,表示此时你期望一个数组,并为其定义特定的处理方式 :) 这样做有益处,可以验证DTO与接收到的数据,以及能够读取数组属性和数组内对象属性(例如,他们想将nodes.edges.length重新映射为products.count - Icepickle
2个回答

2

源数组的位置是清楚明确的,但目标数组的具体位置却存在歧义。因此,在目标映射中放置[]以表示数组的位置。

const data = { foo: "bar", collection: { title: "BestSellers", }, just: { an: { example: "bar", }, }, nodes: { edges: [ { title: "Orange juice", slug: "orange-juice", }, { title: "Without a slug" }, { slug: "without-a-title" }, { title: "Apple juice", slug: "apple-juice", }, ], }, };
const transformer = {
  "collection.title": "section.title",
  "nodes.edges.title": "products.items[].title",
  "nodes.edges.slug": "products.items[].seo.slug",
};

function getAtPath(path, data) {
  let s = path.split('.');
  let v = data[s[0]];
  if(s.length===1) return v;
  let q = s.slice(1).join('.');
  return Array.isArray(v) ? v.map(i=>getAtPath(q, i)) : getAtPath(q, v);
}
function setAtPath(path, value, dest) {
  let s = path.split('.'), d = dest;
  for(let [i,e] of s.entries()) {
    if(i === s.length-1) d[e] = value;
    else {
      if(e.endsWith('[]')) {
        e = e.substring(0, e.length-2);
        value.forEach((j,k) => setAtPath(`${k}.${s.slice(i+1).join('.')}`, j, d[e]??=[]));
        break;
      }
      else d = d[e]??={};
    }
  }
}
function transform(transformer, data) {
  let r = {};
  Object.entries(transformer).forEach(([x,y])=>setAtPath(y,getAtPath(x, data),r));
  return r;
}
console.log(transform(transformer, data));


1

我认为,更容易的部分是改变您定义特定DTO的方式,如果现在仍然可能的话。

如果您在定义中包含了一个项目应该是一个数组的期望,您可以编写特定的处理程序。

在您的示例中,我已经更改了转换器的定义:

const transformer = {
  "collection.title": "section.title",
  "nodes.edges.length": "products.count",
  "nodes.edges[].title": "products.items[].title",
  "nodes.edges[].slug": "products.items[].seo.slug",
};

然后我将处理方式从lodash更改为定制的特定工作的处理方式(我不知道lodash是否已经有了这个处理方式,我没有查看那部分)。

基本上,如果在获取过程中检测到属性名称在其末尾附近有一个[]符号,它将返回一个字符串列表(我还没有测试如果数组中有数组会发生什么,以及如何处理),然后在设置期间,它将根据需要创建一个条目并填充该条目。

const data = {
  foo: "bar",
  collection: {
    title: "BestSellers",
  },
  just: {
    an: {
      example: "bar",
    },
  },
  nodes: {
    edges: [
      {
        title: "Orange juice",
        slug: "orange-juice",
      },
      {
        title: "Without a slug"
      },
      {
        slug: "without-a-title"
      },
      {
        title: "Apple juice",
        slug: "apple-juice",
      },
    ],
  },
};

let transformed = {};

const transformer = {
  "collection.title": "section.title",
  "nodes.edges.length": "products.count",
  "nodes.edges[].title": "products.items[].title",
  "nodes.edges[].slug": "products.items[].seo.slug",
};

function getPropertyAndRemaindingPath( path ) {
  if (!path) {
    return ['', ''];
  }
  const [property, ...rest] = path.split('.');
  return [property, rest.join('.')];
}

function getFromObject( source, property, remainder ) {
  return get( source[property], remainder );
}

function getFromArray( source, property, remainder ) {
  const arrayProperty = source[property.substring(0, property.length-2)];
  return arrayProperty.reduce( (agg, item) => {
    agg.push( get( item, remainder ) );
    return agg;
  }, []);
}

function getFrom( source, property, remainder ) {
  return property.endsWith('[]') ?
    getFromArray( source, property, remainder ) :
    getFromObject( source, property, remainder );
}

function get( source, path ) {
  if (!path || !source) {
    return source;
  }
  const [property, rest] = getPropertyAndRemaindingPath( path );
  return getFrom( source, property, rest );
}

function setToArray( target, property, value, remainder ) {
  const targetProperty = property.substring(0, property.length-2);
  if (!target[targetProperty]) {
    target[targetProperty] = [];
  }
  const arr = target[targetProperty];
  for (let idx = 0; idx < value.length; idx++) {
    if (!arr[idx]) {
      arr[idx] = {};
    }
    set( arr[idx], remainder, value[idx] );
  }
}

function setToObject( target, property, value, remainder ) {
  if (!target[property]) {
    target[property] = {};
  }
  set( target[property], remainder, value );
}

function setTo( target, property, value, remainder ) {
  if (property.endsWith('[]')) {
    setToArray( target, property, value, remainder );
    return;
  } 
  setToObject( target, property, value, remainder );
}

function set( target, path, value ) {
  if (!target) {
    throw new Error('target does not exist');
  }
  const [property, pathRemaining] = getPropertyAndRemaindingPath( path );
  if (!pathRemaining) {
    target[property] = value;
    return;
  }
  setTo( target, property, value, pathRemaining );
}

const _ = {
  get,
  set
};

Object.keys(transformer).forEach((key) => {
  const value = _.get(data, key);
  const path = transformer[key];

  _.set(transformed, path, value);
});

console.log(transformed);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接