Skip to content

Instantly share code, notes, and snippets.

@ksm2
Last active July 18, 2019 21:06
Show Gist options
  • Save ksm2/aba18c9e891e8f681ccc1244b874cd6d to your computer and use it in GitHub Desktop.
Save ksm2/aba18c9e891e8f681ccc1244b874cd6d to your computer and use it in GitHub Desktop.
Config Parser for Node.js
import Yaml from 'js-yaml'
import 'reflect-metadata'
export interface SettingOptions {
nullable?: boolean
}
export const Setting = ({ nullable = false }: SettingOptions = {}): PropertyDecorator => (target, propertyKey) => {
let properties: string[] = Reflect.getMetadata('config:settings', target)
if (!properties) {
properties = []
Reflect.defineMetadata('config:settings', properties, target)
}
properties.push(propertyKey as string)
Reflect.defineMetadata('config:nullable', nullable, target, propertyKey)
}
export type ObjectType<T> = { new(): T };
const toSnakeCase = (property: string): string => {
return property.replace(/[A-Z]/g, (s) => `_${s.toLowerCase()}`)
}
const ensureString = (value: unknown): string => {
if (typeof value === 'string') {
return value
} else if (typeof value === 'number') {
return String(value)
}
throw new Error('Cannot parse value as string: ' + value)
}
const ensureNumber = (value: unknown): number => {
if (typeof value === 'number') {
return value
}
throw new Error('Cannot parse value as number: ' + value)
}
const ensureBoolean = (value: unknown): boolean => {
if (typeof value === 'boolean') {
return value
}
throw new Error('Cannot parse value as boolean: ' + value)
}
const ensureDate = (value: unknown): Date => {
if (value instanceof Date) {
return value
}
throw new Error('Cannot parse value as date: ' + value)
}
export const parseConfig = <T extends object>(yaml: string, target: ObjectType<T>): T => {
const doc = Yaml.safeLoad(yaml)
console.log(doc)
return parseDoc(doc, target)
}
export const parseDoc = <T extends object>(doc: any, target: ObjectType<T>): T => {
const object = new target()
const properties: string[] = Reflect.getMetadata('config:settings', object) || []
for (const property of properties) {
const subDoc = doc[toSnakeCase(property)]
const type = Reflect.getMetadata('design:type', object, property)
const nullable: boolean = Reflect.getMetadata('config:nullable', object, property)
const prop = (value: any) => Reflect.defineProperty(object, property, { value, enumerable: true })
if ((typeof subDoc === 'undefined' || subDoc === null) && nullable) {
prop(undefined)
} else if (type === String) {
prop(ensureString(subDoc))
} else if (type === Number) {
prop(ensureNumber(subDoc))
} else if (type === Boolean) {
prop(ensureBoolean(subDoc))
} else if (type === Date) {
prop(ensureDate(subDoc))
} else {
console.log(property, type)
prop(parseDoc(subDoc, type))
}
}
return object
}
import { parseConfig, Setting } from './configParser'
const yaml = `
demo:
test: Hello
number: 42
bool: ~
hello_world: "demo"
date: 2019-07-18
`
class Demo {
@Setting()
test!: string
@Setting()
number!: number
@Setting({ nullable: true })
bool?: boolean
@Setting()
helloWorld!: string
}
class Config {
@Setting()
demo!: Demo
@Setting()
date!: Date
}
const config = parseConfig(yaml, Config)
console.dir(config)
if (config.demo.bool !== undefined) {
console.dir(config.demo.bool.toString())
}
import Yaml from 'js-yaml'
import 'reflect-metadata'
export interface SettingOptions {
nullable?: boolean
}
export const Setting = ({ nullable = false }: SettingOptions = {}): PropertyDecorator => (target, propertyKey) => {
let properties: string[] = Reflect.getMetadata('config:settings', target)
if (!properties) {
properties = []
Reflect.defineMetadata('config:settings', properties, target)
}
properties.push(propertyKey as string)
Reflect.defineMetadata('config:nullable', nullable, target, propertyKey)
}
export type ObjectType<T> = { new(): T };
const toSnakeCase = (property: string): string => {
return property.replace(/[A-Z]/g, (s) => `_${s.toLowerCase()}`)
}
const ensureString = (value: unknown): string => {
if (typeof value === 'string') {
return value
} else if (typeof value === 'number') {
return String(value)
}
throw new Error('Cannot parse value as string: ' + value)
}
const ensureNumber = (value: unknown): number => {
if (typeof value === 'number') {
return value
}
throw new Error('Cannot parse value as number: ' + value)
}
const ensureBoolean = (value: unknown): boolean => {
if (typeof value === 'boolean') {
return value
}
throw new Error('Cannot parse value as boolean: ' + value)
}
const ensureDate = (value: unknown): Date => {
if (value instanceof Date) {
return value
}
throw new Error('Cannot parse value as date: ' + value)
}
export const parseConfig = <T extends object>(yaml: string, target: ObjectType<T>): T => {
const doc = Yaml.safeLoad(yaml)
console.log(doc)
return parseDoc(doc, target)
}
export const parseDoc = <T extends object>(doc: any, target: ObjectType<T>): T => {
const object = new target()
const properties: string[] = Reflect.getMetadata('config:settings', object) || []
for (const property of properties) {
const subDoc = doc[toSnakeCase(property)]
const type = Reflect.getMetadata('design:type', object, property)
const nullable: boolean = Reflect.getMetadata('config:nullable', object, property)
const prop = (value: any) => Reflect.defineProperty(object, property, { value, enumerable: true })
if ((typeof subDoc === 'undefined' || subDoc === null) && nullable) {
prop(undefined)
} else if (type === String) {
prop(ensureString(subDoc))
} else if (type === Number) {
prop(ensureNumber(subDoc))
} else if (type === Boolean) {
prop(ensureBoolean(subDoc))
} else if (type === Date) {
prop(ensureDate(subDoc))
} else {
console.log(property, type)
prop(parseDoc(subDoc, type))
}
}
return object
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment