Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules/
/.idea/
163 changes: 97 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,72 +28,103 @@ var schema = generator(pkg)
The schema will look like:

```json
[
{
"name": "name",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "version",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "main",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "repository",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "type",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "url",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
{
"name": "bugs",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "url",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
{
"name": "homepage",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "keywords",
"type": "STRING",
"mode": "REPEATED"
},
{
"name": "author",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "license",
"type": "STRING",
"mode": "NULLABLE"
}
]
{
"fields": [
{
"name": "name",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "version",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "main",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "repository",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "type",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "url",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
{
"name": "bugs",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "url",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
{
"name": "homepage",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "keywords",
"type": "STRING",
"mode": "REPEATED"
},
{
"name": "author",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "license",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "scripts",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "test",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
{
"name": "devDependencies",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "chai",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "mocha",
"type": "STRING",
"mode": "NULLABLE"
}
]
}
]
}
```

## License
Expand Down
106 changes: 79 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,101 @@
"use strict"

module.exports = function generate(data) {
return traverse([], data)
return traverse(data)
}


function getMode(val) {
if(Array.isArray(val)) return "REPEATED"
if (Array.isArray(val)) return "REPEATED"
else return "NULLABLE"
}


const typeHierarchy = {
BOOLEAN: 1,
INTEGER: 2,
FLOAT: 3,
TIME: 4,
DATE: 5,
DATETIME: 6,
TIMESTAMP: 7,
STRING: 8,
RECORD: 9,
}

function typeHierarchyReducer(accumulator, currentValue) {
if (accumulator === null) return currentValue;
const comp = typeHierarchy[currentValue] - typeHierarchy[accumulator];
if (comp === 0) return accumulator;
if (comp > 0) return currentValue;
return accumulator;
}

function getType(val) {
if(typeof val === 'boolean') return "BOOLEAN"
if(!isNaN(val)){
if(Number.isInteger(parseFloat(val))) return "INTEGER"
return "FLOAT"
if (typeof val === 'boolean' || (typeof val === 'string' && ["true", "false", "t", "f", "yes", "no", "y", "n"].includes(val.toLowerCase()))) return "BOOLEAN"
if (!isNaN(val)) {
if (Number.isInteger(parseFloat(val))) return "INTEGER"
return "FLOAT"
}
if(val instanceof Date) return "TIMESTAMP"
if(Array.isArray(val)) return getType(val[0])
if(typeof val === 'object') return "RECORD"
if (val instanceof Date) return "TIMESTAMP"
// This works because you can't have an array of arrays.
if (Array.isArray(val)) return val.filter(valItem => valItem !== null).map(valItem => getType(valItem)).reduce(typeHierarchyReducer, null);
if (typeof val === 'object') return "RECORD"

if(typeof val === 'string') {
if(val.match(/\d{4}-\d{2}-\d{2}/)) return "DATE"
if(val.length > 18 && !isNaN((new Date(val)).getTime())) return "TIMESTAMP"
if (typeof val === 'string') {
if (val.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) return "DATE"
if (val.match(/^\d{2}:\d{2}:\d{2}(\.\d{1,6})?$/)) return "TIME"
if (val.match(/^\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2}:\d{2}(\.\d{1,6})?$/)) return "DATETIME"
if (val.match(/^\d{4}[-/]\d{1,2}[-/]\d{1,2}[ T]\d{2}:\d{2}(:\d{2}(\.\d{1,6})?)?/)) return "TIMESTAMP"
}

return "STRING"
}


function traverse(arr, data) {
return Object.keys(data).map((key) => {
let val = data[key]
let meta = {
name: key,
type: getType(data[key]),
mode: getMode(data[key])
function mergeSchemas(original, generatedSchema) {
// console.log(JSON.stringify(original));
// console.log(JSON.stringify(generatedSchema));
for (const field of generatedSchema.fields) {
const origField = original.fields.find(({name}) => name === field.name);
if (origField) {
if (origField.type === 'RECORD' && field.type === 'RECORD') {
mergeSchemas(origField, field);
} else {
origField.type = typeHierarchyReducer(origField.type, field.type);
origField.mode = origField.mode === "REPEATED" || field.mode === "REPEATED" ? "REPEATED" : "NULLABLE"
}
} else {
original.fields.push(field);
}

if(meta.type === 'RECORD') {
meta.fields = traverse([], (meta.mode === 'REPEATED') ? val[0] : val)
}
return original;
}

function traverse(data) {
if (Array.isArray(data)) {
// We have to merge all the objects with the same keys, that forces us to create a hierarchy of types
return data.filter(dataItem => dataItem !== null).map(dataItem => traverse(dataItem)).reduce(mergeSchemas, {fields: []});
} else {
// We filter the fields with null value because we don't need them.
return {
fields: Object.keys(data)
.filter(key => data[key] !== null && (!Array.isArray(data[key]) || (data[key].length > 0 && data[key].find(item => item !== null)) && (typeof data[key] !== 'string' || data[key] !== '')))
.map(key => {
let val = data[key]
let meta = {
name: key,
type: getType(val),
mode: getMode(val),
// original: JSON.stringify(val)
}

if (meta.type === 'RECORD') {
meta.fields = traverse(val).fields
}

return meta
})
}

return meta
})

}

}
Loading