utils/create-schema.js (129 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
// Create a schema (in https://json-schema.org/ format) from the YAML schema
// files at https://github.com/elastic/ecs/tree/master/schemas for use in
// validation tests.
//
// Usage:
// 1. Get a clone and checkout of the intended tag of ecs.git. This tag should
// match the "version" at "../helpers/lib/index.js":
// git clone git@github.com:elastic/ecs.git
// cd ecs
// git checkout TAG # e.g. v1.5.0
// 2. Re-generate the JSON schema file:
// cd .../ecs-logging-nodejs/utils
// npm install
// node create-schema.js .../ecs
// 3. Run the tests and commit the updated schema.
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const ecsRepo = process.argv[2]
if (!ecsRepo) {
usageError('missing $ecsRepo arg (path to a clone of elastic/ecs.git)')
} else if (!fs.existsSync(ecsRepo)) {
usageError(`the given $ecsRepo dir does not exist: "${ecsRepo}"`)
}
const ecsSchemasDir = path.join(ecsRepo, 'schemas')
// Build the JSON schema properties from the ECS schema YAML files.
var properties = getAllFiles(ecsSchemasDir)
.filter(file => !file.includes('README.md'))
.map(file => fs.readFileSync(file, 'utf8'))
.map(yaml.safeLoad)
.filter(entry => Array.isArray(entry)) // filter out weird `{name: 'main', ...}` entry
.reduce((acc, [val]) => {
let properties = {}
for (const prop of val.fields) {
properties = set(properties, prop.name, jsonSchemaTypeFromEcsType(prop.type))
}
if (val.name === 'base') {
Object.assign(acc, properties)
} else {
acc[val.name] = {
type: 'object',
properties
}
}
return acc
}, {})
// Write out a JSON schema file.
const gitInfo = execSync('git log -1 --pretty=format:\'commit %h%d\'', {
cwd: ecsRepo
})
const comment = `ecs.git ${gitInfo}`
const jsonSchema = JSON.stringify({
$comment: comment,
type: 'object',
properties,
additionalProperties: true
}, null, 2)
const outSchemaFile = 'schema.json'
fs.writeFileSync(outSchemaFile, jsonSchema, 'utf8')
console.log(`wrote ${outSchemaFile} (${comment})`)
function getAllFiles (dir) {
return fs.readdirSync(dir).reduce((files, file) => {
const name = path.join(dir, file)
const isDirectory = fs.statSync(name).isDirectory()
return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name]
}, [])
}
function set (object, objPath, value, customizer) {
objPath = objPath
.split('.')
.join('.properties.')
.split('.')
let index = -1
const length = objPath.length
const lastIndex = length - 1
let nested = object
while (nested != null && ++index < length) {
const key = objPath[index]
let newValue = value
if (index !== lastIndex) {
const objValue = nested[key]
newValue = objValue || {}
}
if (key === 'properties') {
nested.type = 'object'
nested.additionalProperties = true
}
nested[key] = newValue
nested = nested[key]
}
return object
}
function jsonSchemaTypeFromEcsType (type) {
switch (type) {
case 'keyword':
case 'constant_keyword':
return { type: 'string' }
case 'boolean':
return { type: 'boolean' }
case 'date':
return { type: 'string', format: 'date-time' }
case 'ip':
return {
anyOf: [
{ type: 'string', format: 'ipv4' },
{ type: 'string', format: 'ipv6' }
]
}
case 'text':
case 'match_only_text':
case 'wildcard':
return { type: 'string' }
case 'integer':
return { type: 'integer' }
case 'long':
case 'float':
case 'scaled_float':
return { type: 'number' }
case 'geo_point':
return {
type: 'object',
properties: {
lat: { type: 'number' },
lon: { type: 'number' }
}
}
case 'object':
case 'flattened':
case 'nested':
case 'source':
return {
type: 'object',
additionalProperties: true
}
default:
throw new Error(`Can't handle the type '${type}'`)
}
}
function usageError (msg) {
const prog = path.basename(process.argv[1])
process.stderr.write(`${prog}: error: ${msg}\n`)
process.stderr.write('usage: node create-schema $ecsRepo\n')
process.exit(1)
}