├── .gitignore ├── src ├── .npmignore ├── interpret │ ├── index.ts │ ├── lex.ts │ ├── types.ts │ ├── syntax.yml │ ├── parse.ts │ ├── syntax-parser.ts │ └── lex.test.ts ├── validate │ ├── types.ts │ ├── validators.ts │ ├── index.ts │ └── index.test.ts ├── util │ └── suggest.ts ├── index.ts └── index.test.ts ├── test └── validManifest.txt ├── .eslintrc.js ├── package.json ├── README.md └── DOCS.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /src/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.js 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /test/validManifest.txt: -------------------------------------------------------------------------------- 1 | TEST : String | 2 | -------------------------------------------------------------------------------- /src/interpret/index.ts: -------------------------------------------------------------------------------- 1 | import parse from "./parse"; 2 | import lex from "./lex"; 3 | 4 | export default (input: string): ReturnType => parse(lex(input)); 5 | -------------------------------------------------------------------------------- /src/validate/types.ts: -------------------------------------------------------------------------------- 1 | type Validators = Record any>; 2 | 3 | type ValidationError = { 4 | error$: string; 5 | }; 6 | 7 | type Env = Record; 8 | 9 | export { Validators, ValidationError, Env }; 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ], 6 | "rules": { 7 | "quotes": [1, "double"], 8 | "no-nested-ternary": 0, 9 | "no-confusing-arrow": 0, 10 | "padded-blocks": 0, 11 | "no-else-return": 0, 12 | "brace-style": 0, 13 | "comma-dangle": 0, 14 | "arrow-body-style": 0 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/interpret/lex.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import yaml from "js-yaml"; 4 | import parser from "./syntax-parser"; 5 | import { Syntax } from "./types"; 6 | 7 | const syntaxPath = path.join(__dirname, "./syntax.yml"); 8 | const syntaxYml = fs.readFileSync(syntaxPath, { encoding: "utf8" }); 9 | 10 | const syntax = yaml.load(syntaxYml) as Syntax; 11 | 12 | const lex = (chars: string): ReturnType => 13 | parser(syntax, "Noop", chars); 14 | 15 | export default lex; 16 | -------------------------------------------------------------------------------- /src/interpret/types.ts: -------------------------------------------------------------------------------- 1 | type Definition = { 2 | name: string; 3 | type?: string; 4 | default?: string; 5 | }; 6 | 7 | type Syntax = Record< 8 | State, 9 | { 10 | [pattern: string]: (State | Action) | (State | Action)[]; 11 | } 12 | >; 13 | 14 | type State = 15 | | "Noop" 16 | | "DeclarationName" 17 | | "DeclarationSeparator" 18 | | "DeclarationSeparatorEnd" 19 | | "DeclarationType" 20 | | "DeclarationTypeEnd" 21 | | "DefaultOrOptional" 22 | | "DeclarationDefaultStart" 23 | | "DeclarationDefaultUnquoted" 24 | | "DeclarationDefaultQuoted" 25 | | "DeclarationDefault" 26 | | "Comment"; 27 | 28 | type Action = "take" | "save" | "trim" | "skip" | "success"; 29 | 30 | type Patterns = Record<"EOF" | "WS" | "EOL" | "_", string>; 31 | 32 | type Token = { type: State; value: string }; 33 | 34 | export type { Definition, Syntax, Token, State, Action, Patterns }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "varium", 3 | "version": "3.0.1", 4 | "type": "module", 5 | "description": "Declare and validate environment variables", 6 | "engines": { 7 | "node": ">=6.5" 8 | }, 9 | "main": "src/index.ts", 10 | "scripts": { 11 | "test": "mocha --recursive './{,!(node_modules)/**/}*.test.ts'" 12 | }, 13 | "keywords": [ 14 | "env", 15 | "environment", 16 | "manifest", 17 | "config", 18 | "settings", 19 | "var", 20 | "variable" 21 | ], 22 | "author": "Andreas Hultgren", 23 | "license": "ISC", 24 | "dependencies": { 25 | "js-yaml": "^3.13.1", 26 | "ramda": "^0.26.1" 27 | }, 28 | "peerDependencies": { 29 | "debug": "2.x" 30 | }, 31 | "devDependencies": { 32 | "@types/chai": "^5.2.2", 33 | "@types/js-yaml": "^4.0.9", 34 | "@types/mocha": "^10.0.10", 35 | "@types/node": "^24.0.7", 36 | "@types/ramda": "^0.30.2", 37 | "chai": "^3.5.0", 38 | "eslint": "^6.1.0", 39 | "eslint-config-airbnb-base": "^11.2.0", 40 | "eslint-plugin-import": "^2.6.1", 41 | "mocha": "^6.1.4", 42 | "typescript": "^5.8.3" 43 | }, 44 | "files": [ 45 | "/src" 46 | ], 47 | "repository": "ahultgren/node-varium" 48 | } 49 | -------------------------------------------------------------------------------- /src/interpret/syntax.yml: -------------------------------------------------------------------------------- 1 | validators: 2 | - &validDeclarationName /[a-zA-Z0-9_]/i 3 | - &validDefaultValue /[^#\n]/ 4 | 5 | Noop: 6 | [*validDeclarationName]: DeclarationName 7 | /#/: Comment 8 | EOF: success 9 | WS: skip 10 | 11 | DeclarationName: 12 | [*validDeclarationName]: take 13 | / |:/: [save, DeclarationSeparator] 14 | DeclarationSeparator: 15 | / /: skip 16 | /:/: [skip, DeclarationSeparatorEnd] 17 | DeclarationSeparatorEnd: 18 | / /: skip 19 | /[A-Z]/i: DeclarationType 20 | DeclarationType: 21 | /[A-Z]/i: take 22 | _: [save, DeclarationTypeEnd] 23 | DeclarationTypeEnd: 24 | / /: skip 25 | /\|/: [skip, DefaultOrOptional] 26 | /#/: Noop 27 | /\n/: Noop 28 | EOF: Noop 29 | 30 | DefaultOrOptional: 31 | / /: skip 32 | [*validDefaultValue]: DeclarationDefaultStart 33 | EOF: DeclarationDefault 34 | EOL: DeclarationDefault 35 | /#/: DeclarationDefault 36 | DeclarationDefaultStart: 37 | /"/: [skip, DeclarationDefaultQuoted] 38 | [*validDefaultValue]: DeclarationDefaultUnquoted 39 | DeclarationDefaultUnquoted: 40 | [*validDefaultValue]: take 41 | _: [trim, DeclarationDefault] 42 | DeclarationDefaultQuoted: 43 | /\\/: [skip, take] 44 | /"/: [skip, DeclarationDefault] 45 | _: take 46 | DeclarationDefault: 47 | _: [save, Noop] 48 | 49 | Comment: 50 | EOL: Noop 51 | EOF: Noop 52 | _: skip 53 | -------------------------------------------------------------------------------- /src/validate/validators.ts: -------------------------------------------------------------------------------- 1 | import { Validators } from "./types"; 2 | 3 | const validators: Validators = { 4 | String: (value: string) => value, 5 | Int: (value: string) => { 6 | const validValue = parseInt(value, 10); 7 | 8 | if (value === "") { 9 | return undefined; 10 | } 11 | 12 | if ( 13 | typeof validValue === "number" && 14 | (isNaN(validValue) || String(validValue) !== value) 15 | ) { 16 | throw new Error("value is not a valid Int"); 17 | } 18 | 19 | return validValue; 20 | }, 21 | Float: (value: string) => { 22 | const validValue = parseFloat(value); 23 | 24 | if (value === "") { 25 | return undefined; 26 | } 27 | 28 | if ( 29 | typeof validValue === "number" && 30 | (isNaN(validValue) || isNaN(value as unknown as number)) 31 | ) { 32 | throw new Error("value is not a valid Float"); 33 | } 34 | 35 | return validValue; 36 | }, 37 | Bool: (value: string) => { 38 | if (value === "") { 39 | return undefined; 40 | } else if (value === "false") { 41 | return false; 42 | } else if (value === "true") { 43 | return true; 44 | } else { 45 | throw new Error("value is not a valid Bool"); 46 | } 47 | }, 48 | Json: (value: string) => { 49 | try { 50 | return value === "" ? undefined : JSON.parse(value); 51 | } catch (e) { 52 | throw new Error("value is not a valid Json"); 53 | } 54 | }, 55 | }; 56 | 57 | export default validators; 58 | -------------------------------------------------------------------------------- /src/interpret/parse.ts: -------------------------------------------------------------------------------- 1 | import { Definition, Token } from "./types"; 2 | 3 | function duplicatedDefinitions(manifest: Definition[]) { 4 | const duplicates: string[] = []; 5 | const duplicateMap = {}; 6 | 7 | manifest.forEach((definition) => { 8 | if (duplicateMap[definition.name]) { 9 | duplicates.push(definition.name); 10 | } 11 | duplicateMap[definition.name] = 1; 12 | }); 13 | 14 | return duplicates; 15 | } 16 | 17 | function parseTokens(definitions: Definition[], token: Token): Definition[] { 18 | /* eslint no-param-reassign: 0 */ 19 | if (token.type === "DeclarationName") { 20 | return [ 21 | ...definitions, 22 | { 23 | name: token.value, 24 | type: "", 25 | }, 26 | ]; 27 | } else if (token.type === "DeclarationType") { 28 | definitions[definitions.length - 1].type = token.value; 29 | } else if (token.type === "DeclarationDefault") { 30 | definitions[definitions.length - 1].default = token.value; 31 | } 32 | return definitions; 33 | } 34 | 35 | export default (tokens: Token[]) => { 36 | const definitions = tokens.reduce(parseTokens, []); 37 | const duplicated = duplicatedDefinitions(definitions); 38 | 39 | if (duplicated.length) { 40 | const stack = duplicated 41 | .map((name) => ` Env var ${name} is declared more than once.`) 42 | .join("\n"); 43 | const err = new Error("Varium: Error reading manifest"); 44 | err.stack = stack; 45 | throw err; 46 | } else { 47 | return definitions; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/util/suggest.ts: -------------------------------------------------------------------------------- 1 | const levDistance = (a: string, b: string): number => { 2 | if (typeof a === "undefined") return 9000; 3 | if (typeof b === "undefined") return 9000; 4 | if (a.length === 0) return b.length; 5 | if (b.length === 0) return a.length; 6 | 7 | if (a.toLowerCase() === b.toLowerCase()) return 0; 8 | 9 | const matrix: number[][] = []; 10 | 11 | // increment along the first column of each row 12 | let i: number; 13 | for (i = 0; i <= b.length; i += 1) { 14 | matrix[i] = [i]; 15 | } 16 | 17 | // increment each column in the first row 18 | let j: number; 19 | for (j = 0; j <= a.length; j += 1) { 20 | matrix[0][j] = j; 21 | } 22 | 23 | // Fill in the rest of the matrix 24 | for (i = 1; i <= b.length; i += 1) { 25 | for (j = 1; j <= a.length; j += 1) { 26 | if (b.charAt(i - 1) === a.charAt(j - 1)) { 27 | matrix[i][j] = matrix[i - 1][j - 1]; 28 | } else { 29 | matrix[i][j] = Math.min( 30 | matrix[i - 1][j - 1] + 1, // substitution 31 | Math.min( 32 | matrix[i][j - 1] + 1, // insertion 33 | matrix[i - 1][j] + 1 34 | ) 35 | ); // deletion 36 | } 37 | } 38 | } 39 | 40 | return matrix[b.length][a.length]; 41 | }; 42 | 43 | const sortByProp = (prop: string, list: any[]): any[] => { 44 | return list.slice(0).sort((a, b) => { 45 | return a[prop] < b[prop] ? -1 : a[prop] > b[prop] ? 1 : 0; 46 | }); 47 | }; 48 | 49 | const suggestName = (alternatives: string[], search: string): string[] => { 50 | const possibleMatches = alternatives 51 | .map((name) => ({ 52 | name, 53 | distance: levDistance(name, search), 54 | })) 55 | .filter((alternative) => alternative.distance < 3); 56 | 57 | return sortByProp("distance", possibleMatches).map( 58 | (alternative) => alternative.name 59 | ); 60 | }; 61 | 62 | export default (alternatives: string[], search: string): string => { 63 | const suggestions = suggestName(alternatives, search); 64 | 65 | if (suggestions.length === 0) { 66 | return "Unable to offer any suggestions."; 67 | } else if (suggestions.length === 1) { 68 | return `Maybe you meant ${suggestions[0]}?`; 69 | } else { 70 | return `Maybe you meant one of these: ${suggestions.join("|")}`; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/validate/index.ts: -------------------------------------------------------------------------------- 1 | import defaultValidators from "./validators"; 2 | import validatorError from "../util/suggest"; 3 | import { Env, ValidationError, Validators } from "./types"; 4 | import { Definition } from "../interpret/types"; 5 | 6 | let logName; 7 | let logValue; 8 | 9 | try { 10 | // eslint-disable-next-line 11 | const debug = await import("debug"); 12 | logName = debug("varium:validate:name"); 13 | logValue = debug("varium:validate:value"); 14 | } catch (e) { 15 | logName = () => {}; 16 | logValue = () => {}; 17 | } 18 | 19 | export default function validate( 20 | customValidators: Validators | undefined | null, 21 | manifest: Definition[], 22 | env: NodeJS.ProcessEnv 23 | ): (ValidationError | Env)[] { 24 | const validators = Object.assign({}, defaultValidators, customValidators); 25 | 26 | return manifest.map((definition) => { 27 | const validator = validators[definition.type!]; 28 | const envValue = env[definition.name]; 29 | const envDefault = definition.default; 30 | 31 | if (!validator) { 32 | const errorMessage = validatorError( 33 | Object.keys(validators), 34 | definition.type! 35 | ); 36 | 37 | return { 38 | error$: `The type ${definition.type} for env var "${definition.name}" does not exist.\n${errorMessage}`, 39 | }; 40 | } 41 | 42 | if (envValue === undefined && envDefault === undefined) { 43 | return { 44 | error$: `Env var "${definition.name}" requires a value.`, 45 | }; 46 | } 47 | 48 | logName(definition.name); 49 | logValue(`Value: ${envValue}`); 50 | logValue(`Default: ${envDefault}`); 51 | 52 | if (envDefault !== undefined) { 53 | try { 54 | validator(envDefault); 55 | } catch (e) { 56 | return { 57 | error$: `Default value for "${definition.name}" is invalid: ${e.message}`, 58 | }; 59 | } 60 | } 61 | 62 | const value = 63 | envValue === undefined || envValue === "" ? envDefault : envValue; 64 | 65 | try { 66 | return { 67 | [definition.name]: validator(value), 68 | }; 69 | } catch (e) { 70 | return { 71 | error$: `Value for "${definition.name}" is invalid: ${e.message}`, 72 | }; 73 | } 74 | }); 75 | } 76 | 77 | export { defaultValidators }; 78 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import interpret from "./interpret"; 4 | import validate from "./validate"; 5 | import nameError from "./util/suggest"; 6 | import { Validators } from "./validate/types"; 7 | 8 | export { interpret, validate, nameError }; 9 | 10 | type Config = { 11 | types?: Validators; 12 | noProcessExit?: boolean; 13 | }; 14 | 15 | function reader( 16 | config: Config, 17 | env: NodeJS.ProcessEnv, 18 | manifestString: string 19 | ): Record { 20 | const result = validate(config.types, interpret(manifestString), env); 21 | const errors = result.map((x) => x.error$).filter(Boolean); 22 | 23 | if (errors.length) { 24 | const msg = "Varium: Error reading env:"; 25 | const stack = `${msg}\n ${errors.join("\n ")}`; 26 | 27 | if (config.noProcessExit) { 28 | const err = new Error(msg); 29 | err.stack = stack; 30 | throw err; 31 | } else { 32 | /* eslint no-console: 0 */ 33 | console.error(stack); 34 | process.exit(1); 35 | } 36 | } 37 | 38 | const values = Object.assign.apply(null, [{}].concat(result)); 39 | 40 | return new Proxy(values, { 41 | get(target, prop) { 42 | if (!Object.prototype.hasOwnProperty.call(target, prop)) { 43 | if (prop === "get") { 44 | return (name) => { 45 | throw new Error( 46 | `Varium upgrade notice: config.get("${name}") is obsolete. Access the property directly using config.${name}` 47 | ); 48 | }; 49 | } else { 50 | const suggestion = nameError(Object.keys(values), prop as string); 51 | throw new Error( 52 | `Varium: Undeclared env var '${String(prop)}'.\n${suggestion}` 53 | ); 54 | } 55 | } else { 56 | return target[prop]; 57 | } 58 | }, 59 | }); 60 | } 61 | 62 | const loader = (manifestPath: string) => { 63 | const appDir = path.dirname(require?.main?.filename ?? process.cwd() + "/_"); 64 | const absPath = path.resolve(appDir, manifestPath); 65 | 66 | try { 67 | return fs.readFileSync(absPath, { encoding: "utf8" }); 68 | } catch (e) { 69 | throw new Error(`Varium: Could not find a manifest at ${absPath}`); 70 | } 71 | }; 72 | 73 | interface VariumOptions { 74 | types?: Validators; 75 | env?: NodeJS.ProcessEnv; 76 | manifestPath?: string; 77 | noProcessExit?: boolean; 78 | } 79 | 80 | function varium({ 81 | types = {}, 82 | env = process.env, 83 | manifestPath = "env.manifest", 84 | noProcessExit = false, 85 | }: VariumOptions = {}): unknown { 86 | return reader({ types, noProcessExit }, env, loader(manifestPath)); 87 | } 88 | 89 | export default varium; 90 | export { reader }; 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Varium 2 | 3 | Varium is a library and syntax for managing environment variables in a sane way. 4 | You should use it if you want to: 5 | 6 | - **declare** all used environment variables in **one place** 7 | - **specify** which **types** they have 8 | - **validate** that they are of the right type 9 | - **cast environment variables** to the right type when used 10 | - **require** certain variables 11 | - **default** to a value for other variables 12 | - **abort CI** if variables are missing or fail validation 13 | - **warn developers** if they use an undeclared environment variable 14 | 15 | _**Note**: Version 3 only supports Bun and other typescript-native environments, as no js-files or .d.ts files are generated. Feel free to PR if you need this, or use v2.0.6._ 16 | 17 | ## Installation 18 | 19 | `npm install varium --save` 20 | 21 | _Requires node v6.5 or above._ 22 | 23 | ## Usage example 24 | 25 | Create a file called `env.manifest` in the project root. It should contain all 26 | environment variables used in the project. For example: 27 | 28 | ``` 29 | API_BASE_URL : String 30 | API_SECRET : String 31 | 32 | # This is a comment 33 | # The following is an optional variable (the above were required): 34 | NUMBER_OF_ITEMS : Int | 35 | 36 | FLAG : Bool | False # Variables can also have default values. Here it is False 37 | COMPLEX_VALUE : Json | [{ "object": 42 }] # Use json for advanced data structures 38 | 39 | QUOTED_STRING : String | "Quote the string if it contains # or \\escaped chars" 40 | ``` 41 | 42 | Then create the file which all your other files imports to obtain the config. 43 | For example `config/index.js`. This needs to at least contain: 44 | 45 | ```js 46 | const varium = require("varium"); 47 | 48 | module.exports = varium(); 49 | ``` 50 | 51 | Import this file in the rest of your project to read environment variables: 52 | 53 | ```js 54 | const config = require("../config"); 55 | const url = config.API_BASE_URL; 56 | 57 | // An error will be thrown if you try to load an undeclared variable: 58 | const wrong = config.API_BASE_ULR; 59 | // -> Error('Varium: Undeclared env var "API_BASE_ULR.\nMaybe you meant API_BASE_URL?"') 60 | ``` 61 | 62 | To prevent other developers or your future self from using `process.env` 63 | directly, use the `no-process-env` 64 | [eslint rule](https://eslint.org/docs/rules/no-process-env). 65 | 66 | Your environment now needs to contain the required variables. If you use a 67 | library to load `.env` files (such as node-forman or dotenv), the `.env` could 68 | contain this: 69 | 70 | ```bash 71 | API_BASE_URL=https://example.com/ 72 | API_SECRET=1337 73 | NUMBER_OF_ITEMS=3 74 | ``` 75 | 76 | To abort builds during CI when environment variables are missing, just run the 77 | config file during th build step. For example, on heroku the following would be 78 | enough: 79 | 80 | ```js 81 | { 82 | "scripts": { 83 | "heroku-postbuild": "node ./config" 84 | } 85 | } 86 | ``` 87 | 88 | For a complete syntax and api reference (for example how to add your own custom 89 | types), see the [docs](./DOCS.md). 90 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | /* global it, describe */ 2 | 3 | import path from "path"; 4 | import { should, expect } from "chai"; 5 | import { default as varium, reader } from "."; 6 | 7 | should(); 8 | 9 | describe("Loader", () => { 10 | it("should load an existing file", () => { 11 | varium({ 12 | manifestPath: path.resolve(__dirname, "../test/validManifest.txt"), 13 | }); 14 | }); 15 | 16 | it("should fail to load a non-existing file", () => { 17 | expect( 18 | varium.bind({ 19 | manifestPath: "test/validManifest-fail.txt", 20 | }) 21 | ).to.throw(); 22 | }); 23 | }); 24 | 25 | describe("Reader", () => { 26 | it("shoud parse all valid formats", () => { 27 | reader( 28 | {}, 29 | { 30 | STRING: "", 31 | INT: "1", 32 | FLOAT: "1.1", 33 | BOOL: "true", 34 | }, 35 | ` 36 | STRING:String 37 | String_1:String| 38 | String2:String|asd 39 | STRING3:String|1 40 | STRING4:String|1#Comment 41 | STRING5:String|"#notComment" 42 | 43 | INT : Int 44 | INT2 : Int | 45 | INT3 : Int | 1 46 | INT4 : Int | "1" 47 | 48 | # "Heading" 49 | 50 | FLOAT: Float # Aka any number 51 | FLOAT1: Float| 0.1 52 | FLOAT2: Float |0.1 53 | FLOAT3: Float |"0.1" 54 | BOOL : Bool 55 | LONG_BOOL2 : Bool | 56 | BOOL3 : Bool | true 57 | BOOL4 : Bool | "false" 58 | ` 59 | ); 60 | }); 61 | 62 | it("should expose the correct value", () => { 63 | const manifest = ` 64 | STRING_REQUIRED : String 65 | STRING_OPTIONAL : String | 66 | STRING_DEFAULT : String | def 67 | `; 68 | const noValues = reader( 69 | {}, 70 | { 71 | STRING_REQUIRED: "str", 72 | }, 73 | manifest 74 | ); 75 | const withValues = reader( 76 | {}, 77 | { 78 | STRING_REQUIRED: "str", 79 | STRING_OPTIONAL: "str", 80 | STRING_DEFAULT: "str", 81 | }, 82 | manifest 83 | ); 84 | 85 | noValues.STRING_REQUIRED.should.equal("str"); 86 | noValues.STRING_OPTIONAL.should.equal(""); 87 | noValues.STRING_DEFAULT.should.equal("def"); 88 | withValues.STRING_OPTIONAL.should.equal("str"); 89 | withValues.STRING_DEFAULT.should.equal("str"); 90 | }); 91 | 92 | it("should throw when accesing undeclared vars", () => { 93 | const config = reader({}, {}, "NAN_EXISTING : String |"); 94 | expect(() => config.NON_EXISTING).to.throw( 95 | "Varium: Undeclared env var 'NON_EXISTING'." + 96 | "\n" + 97 | "Maybe you meant NAN_EXISTING?" 98 | ); 99 | }); 100 | 101 | it("should warn about obsolete usage of .get", () => { 102 | const config = reader({}, {}, ""); 103 | expect(() => config.get("A_VAR")).to.throw( 104 | 'Varium upgrade notice: config.get("A_VAR") is obsolete. Access the property directly using config.A_VAR' 105 | ); 106 | }); 107 | 108 | it("should reject duplicate definitions", () => { 109 | expect(reader.bind(null, {}, {}, "TEST : String\nTEST : Int")).to.throw(); 110 | }); 111 | 112 | it("should hadle EOF", () => { 113 | reader({}, { STRING: "" }, "STRING:String"); 114 | reader({}, {}, "STRING:String|"); 115 | reader({}, {}, "STRING:String|asd"); 116 | reader({}, {}, "STRING:String|asd#"); 117 | reader({}, {}, "STRING:String|asd#asd"); 118 | }); 119 | 120 | it("should handle complex default values", () => { 121 | const config = reader( 122 | {}, 123 | {}, 124 | ` 125 | STR1 : String | A long time ago 126 | STR2 : String | "In a galaxy" 127 | STR3 : String | "full of # signs" 128 | STR4 : String | "and quoted \\"quotes\\"" 129 | STR5 : String | or unquoted "quotes"? 130 | ` 131 | ); 132 | 133 | config.STR1.should.equal("A long time ago"); 134 | config.STR2.should.equal("In a galaxy"); 135 | config.STR3.should.equal("full of # signs"); 136 | config.STR4.should.equal('and quoted "quotes"'); 137 | config.STR5.should.equal('or unquoted "quotes"?'); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/interpret/syntax-parser.ts: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | import { State, Patterns, Syntax, Token, Action } from "./types"; 3 | 4 | let log: (...args: any[]) => void; 5 | 6 | try { 7 | // eslint-disable-next-line 8 | const debug = await import("debug"); 9 | log = debug("varium:lexer"); 10 | } catch (e) { 11 | log = () => {}; 12 | } 13 | 14 | // Hack: convince typescript that R.map maps over objects 15 | const mapObj = R.map as ( 16 | fn: (x: T) => U 17 | ) => (obj: Record) => Record; 18 | 19 | const wrapNonArrays = R.ifElse(Array.isArray, R.identity, (x) => [x]); 20 | const harmonizeSyntax = mapObj(mapObj(wrapNonArrays)); 21 | 22 | const Input = (data: string) => { 23 | let i = 0; 24 | return { 25 | peek: () => (data[i] === undefined ? "EOF" : data[i]), 26 | skip: () => { 27 | i += 1; 28 | }, 29 | eof: () => i > data.length, 30 | pos: () => i, 31 | }; 32 | }; 33 | 34 | const matchCondition = 35 | (patterns: Patterns, char: string) => (condition: string) => { 36 | const pattern = patterns[condition] || condition; 37 | 38 | if (!pattern) { 39 | throw new Error(`Unknown condition ${condition}`); 40 | } 41 | 42 | const parts = pattern.match(/^\/(.*)\/([a-z]*)/); 43 | 44 | if (!parts) { 45 | throw new Error(`Invalid condition ${condition}`); 46 | } 47 | 48 | const safeRegexStr = parts[1][0] === "^" ? parts[1] : `^(${parts[1]})$`; 49 | const regex = new RegExp(safeRegexStr, parts[2]); 50 | 51 | return Boolean(String(char).match(regex)); 52 | }; 53 | 54 | const FailSafe = (lim: number) => { 55 | const limit = Math.max(10, lim); 56 | let lastPos = -1; 57 | let count = 0; 58 | 59 | return (pos) => { 60 | if (pos === lastPos) { 61 | count += 1; 62 | } else { 63 | count = 0; 64 | } 65 | lastPos = pos; 66 | 67 | return count < limit; 68 | }; 69 | }; 70 | 71 | export default (rawSyntax: Syntax, initialState: State, chars: string) => { 72 | const tokens: Token[] = []; 73 | const syntax = harmonizeSyntax(rawSyntax); 74 | const input = Input(chars); 75 | const failSafe = FailSafe(chars.length); 76 | 77 | let done = false; 78 | let currentStateName: State = initialState; 79 | let store = ""; 80 | 81 | const functions: Record void> = { 82 | take() { 83 | store += input.peek(); 84 | input.skip(); 85 | }, 86 | save() { 87 | tokens.push({ 88 | type: currentStateName, 89 | value: store, 90 | }); 91 | store = ""; 92 | }, 93 | trim() { 94 | store = store.trim(); 95 | }, 96 | skip() { 97 | input.skip(); 98 | }, 99 | success() { 100 | done = true; 101 | }, 102 | }; 103 | 104 | const patterns: Patterns = { 105 | EOF: "/EOF/", 106 | WS: "/ |\\t|\\n/", 107 | EOL: "/\\n/", 108 | _: "/(.|\\r\\n|\\r|\\n)*/", 109 | }; 110 | 111 | const doAction = (action: State | Action) => { 112 | if (functions[action]) { 113 | functions[action](); 114 | } else if (syntax[action]) { 115 | currentStateName = action as State; 116 | } else { 117 | throw new Error(`Unknown action ${action}`); 118 | } 119 | }; 120 | 121 | while (!done) { 122 | const currentChar = input.peek(); 123 | const currentState = syntax[currentStateName]; 124 | 125 | const condition = R.find( 126 | matchCondition(patterns, currentChar), 127 | Object.keys(currentState) 128 | ) as string; 129 | const actions = currentState[condition]; 130 | 131 | log(currentStateName, currentChar, actions); 132 | 133 | if (actions) { 134 | actions.forEach(doAction); 135 | } else { 136 | throw new Error(`Unexpected '${currentChar}' in ${currentStateName}`); 137 | } 138 | 139 | if (!failSafe(input.pos())) { 140 | throw new Error( 141 | `Endless cycle detected in state ${currentStateName} for condition ${condition}: ${actions}` 142 | ); 143 | } 144 | 145 | if (input.eof()) { 146 | throw new Error(`Unexpected end of string in state ${currentStateName}`); 147 | } 148 | } 149 | 150 | return tokens; 151 | }; 152 | -------------------------------------------------------------------------------- /src/interpret/lex.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0 */ 2 | /* global it, describe */ 3 | 4 | import { expect } from "chai"; 5 | import lex from "./lex.ts"; 6 | 7 | describe("Lex", () => { 8 | it("should return empty list on empty string", () => { 9 | expect(lex("")).to.eql([]); 10 | }); 11 | 12 | it("should ignore whitespace", () => { 13 | expect(lex(" ")).to.eql([]); 14 | expect(lex(" ")).to.eql([]); 15 | }); 16 | 17 | it("should parse the declaration name & type", () => { 18 | const res = [ 19 | { type: "DeclarationName", value: "THING" }, 20 | { type: "DeclarationType", value: "String" }, 21 | ]; 22 | 23 | expect(lex("THING : String")).to.eql(res); 24 | expect(lex("THING:String")).to.eql(res); 25 | expect(lex(" THING:String ")).to.eql(res); 26 | }); 27 | 28 | it("should parse an empty/optional default", () => { 29 | const res = lex("THING : String |"); 30 | 31 | expect(res).to.have.deep.property("[2].type", "DeclarationDefault"); 32 | expect(res).to.have.deep.property("[2].value", ""); 33 | }); 34 | 35 | it("should parse a default value", () => { 36 | expect(lex("THING : String | Value")).to.have.deep.property( 37 | "[2].value", 38 | "Value" 39 | ); 40 | expect(lex("THING : String |Value")).to.have.deep.property( 41 | "[2].value", 42 | "Value" 43 | ); 44 | expect(lex("THING : String | Value ")).to.have.deep.property( 45 | "[2].value", 46 | "Value" 47 | ); 48 | expect(lex("THING : String | Ö value")).to.have.deep.property( 49 | "[2].value", 50 | "Ö value" 51 | ); 52 | expect( 53 | lex("THING : String|http://example.com?mjau ") 54 | ).to.have.deep.property("[2].value", "http://example.com?mjau"); 55 | }); 56 | 57 | it("should parse a quoted default value", () => { 58 | expect(lex('THING : String | "Value"')).to.have.deep.property( 59 | "[2].value", 60 | "Value" 61 | ); 62 | expect(lex('THING : String | "Value # hash"')).to.have.deep.property( 63 | "[2].value", 64 | "Value # hash" 65 | ); 66 | expect(lex('THING : String | "Value\nnewline"')).to.have.deep.property( 67 | "[2].value", 68 | "Value\nnewline" 69 | ); 70 | expect( 71 | lex('THING : String | "Value\nnewline" #A comment') 72 | ).to.have.deep.property("[2].value", "Value\nnewline"); 73 | expect(lex('THING : String | " spaces "')).to.have.deep.property( 74 | "[2].value", 75 | " spaces " 76 | ); 77 | }); 78 | 79 | it("should parse escaped chars in a quoted default value", () => { 80 | expect(lex('THING : String | "Value\\"quote"')).to.have.deep.property( 81 | "[2].value", 82 | 'Value"quote' 83 | ); 84 | expect(lex('THING : String | "Backslash: \\\\"')).to.have.deep.property( 85 | "[2].value", 86 | "Backslash: \\" 87 | ); 88 | }); 89 | 90 | it("should parse quotes in the middle of a default as chars", () => { 91 | expect(lex('THING : String | Value"quote"')).to.have.deep.property( 92 | "[2].value", 93 | 'Value"quote"' 94 | ); 95 | }); 96 | 97 | it("should parse a comment", () => { 98 | expect(() => lex("THING : String | Value # Comment")).not.to.throw(); 99 | expect(() => 100 | lex("THING : String | Value#Anöthing goes|in a comment!#") 101 | ).not.to.throw(); 102 | expect(() => lex("THING : String # Comment")).not.to.throw(); 103 | }); 104 | 105 | it("should parse multiple declarations", () => { 106 | const res1 = [ 107 | { type: "DeclarationName", value: "ONE" }, 108 | { type: "DeclarationType", value: "String" }, 109 | { type: "DeclarationName", value: "TWO" }, 110 | { type: "DeclarationType", value: "String" }, 111 | ]; 112 | 113 | const res2 = [ 114 | { type: "DeclarationName", value: "ONE" }, 115 | { type: "DeclarationType", value: "String" }, 116 | { type: "DeclarationDefault", value: "Default" }, 117 | { type: "DeclarationName", value: "TWO" }, 118 | { type: "DeclarationType", value: "String" }, 119 | { type: "DeclarationDefault", value: "" }, 120 | ]; 121 | 122 | expect(lex("ONE : String\nTWO : String")).to.eql(res1); 123 | expect(lex("ONE : String # Comment \nTWO : String")).to.eql(res1); 124 | expect( 125 | lex("ONE : String | Default # Comment \n\nTWO : String | #Comment\n") 126 | ).to.eql(res2); 127 | }); 128 | 129 | it("should fail on invalid name", () => { 130 | expect(() => lex("THINGö : String")).to.throw( 131 | "Unexpected 'ö' in DeclarationName" 132 | ); 133 | expect(() => lex("-THING : String")).to.throw("Unexpected '-'"); 134 | expect(() => lex("TH'ING : String")).to.throw("Unexpected '''"); 135 | expect(() => lex(": String")).to.throw("Unexpected ':'"); 136 | }); 137 | 138 | it("should fail with only name", () => { 139 | expect(() => lex("String")).to.throw("Unexpected 'EOF' in DeclarationName"); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # Varium reference 2 | 3 | ## Varium manifest syntax 4 | 5 | ``` 6 | Varium 7 | Syntax: 8 | 9 | # 10 | # 11 | "" 12 | Each line must contain a declaration or be an emtpy line. 13 | Might be followed by a comment. 14 | Whitespace between tokens, except for newlines, is insignificant. 15 | 16 | Declaration 17 | Syntax: 18 | : 19 | : | 20 | : | 21 | A declaration consists of a name and a type. 22 | It might be followed by a default value. 23 | Examples: 24 | A_VAR : String 25 | A_VAR : String | 26 | A_VAR : String | Default value 27 | A_VAR : String | "Default\nvalue" 28 | 29 | Varname 30 | Allowed characters: 31 | /[a-z]+/i 32 | The name of the variable, as found in the environment. May only contain 33 | ascii alfa-numeric characters, and are by convention usually named using 34 | uppercase letters. 35 | Example: 36 | BASE_URL 37 | 38 | Type 39 | Allowed characters: 40 | /[a-z]+/i 41 | The type of the variable, must be one of the built-in types or a custom 42 | type. 43 | Example: 44 | String 45 | 46 | Default 47 | Syntax: 48 | LiteralDefault 49 | QuotedDefault 50 | Either a literal string which is not escaped, or a quoted string which is. 51 | 52 | LiteralDefault 53 | Allowed characters: 54 | /[^"][^#]*/i 55 | May contain any value except newline and hash. 56 | May not start with a quote since that signifies the start of an escaped 57 | string (see below). Whitespace before and after the value is stripped. 58 | Example: 59 | This is a string! 60 | 61 | QuotedDefault 62 | Syntax: 63 | "" 64 | Surround the value with quotes if the value contains "#" or escaped 65 | characters. May contain all other characters except newlines and quotes ("), 66 | which must be escaped. 67 | Example: 68 | "http://example.com/#hash" 69 | 70 | Comment 71 | Allowed characters: /.*/ 72 | May contain any character. Is read until the end of the line. Whitespace 73 | between the "#" character and the first comment character is stripped. 74 | Example: 75 | # This is a comment 76 | ``` 77 | 78 | ## Built-in types 79 | 80 | ``` 81 | Int 82 | A positive or negative integer. 83 | Example: 1000, -3 84 | 85 | Float 86 | A positive or negative float. 87 | Example: 10.1, -101.101, 42 88 | 89 | String 90 | A string of any characters 91 | Example: "This is a string!". 92 | 93 | Bool 94 | It's true or false. 95 | Example: true, false 96 | 97 | Json 98 | Basic support for complex data such as objects and arrays. 99 | Example: [{"name": "value", "number": 1}, [3]] 100 | ``` 101 | 102 | ## JS API 103 | 104 | ``` 105 | varium(Options) -> Config 106 | 107 | Options.types 108 | Add your own custom types or overwrite the built-in ones. See custom types 109 | below. 110 | 111 | Options.env 112 | Provide another object as the environment. By default `process.env` is used. 113 | If you use dotenv, make sure `require('dotenv').config()` is called before 114 | initializing varium. 115 | 116 | Options.manifestPath 117 | Provide another path to the manifest file. The default is 118 | `${projectRootfolder}/env.manifest` where to root is deduced through 119 | `require.main.filename`. Use an absolute path if you want your path to not 120 | be relative to that. 121 | 122 | Options.noProcessExit 123 | Set to true if you do not want the process to exit with an error on 124 | valdiation errors. 125 | 126 | 127 | Config 128 | An object consisting of the declared environment variables. Will throw if 129 | accessing an undeclared environment variable. 130 | 131 | ``` 132 | 133 | ## Custom types 134 | 135 | A custom type is a function that takes one value: the value read from the 136 | environment, or the default value provided from the manifest, if any. It is 137 | always a string, and it's only an empty string if the variable is optional or if 138 | an empty string was provided from the environment (e.g. `BASE_URL=`). 139 | 140 | It returns a proper value according to the type, or throws an error. 141 | 142 | ```js 143 | varium({ 144 | types: { 145 | Url: (value) => { 146 | if (value === "") { 147 | return undefined; 148 | } 149 | 150 | if (!validateUrl(value)) { 151 | throw new Error('Invalid url'); 152 | } else { 153 | return value; 154 | } 155 | } 156 | } 157 | }) 158 | ``` 159 | 160 | ## Logs 161 | 162 | [Debug][debug] is used for logging if installed. Thus if you need to debug 163 | something, set `DEBUG=varium:*`. **Note** that it's not advised to use 164 | this level in production. It logs the environment variable values and may thus 165 | potentially log secrets, which is widely frowned upon. 166 | 167 | [debug]: https://www.npmjs.com/package/debug 168 | -------------------------------------------------------------------------------- /src/validate/index.test.ts: -------------------------------------------------------------------------------- 1 | /* global it, describe */ 2 | 3 | import { should, expect } from "chai"; 4 | import validator from "."; 5 | import { Env } from "./types"; 6 | 7 | should(); 8 | 9 | describe("Validate", () => { 10 | describe("when required", () => { 11 | it("should return error if value is missing", () => { 12 | expect( 13 | validator(null, [{ name: "TEST", type: "String" }], {}) 14 | ).to.deep.equal([{ error$: 'Env var "TEST" requires a value.' }]); 15 | }); 16 | 17 | it("should return error if type doesn't exist", () => { 18 | expect( 19 | validator(null, [{ name: "TEST", type: "Test" }], {}) 20 | ).to.deep.equal([ 21 | { 22 | error$: 23 | 'The type Test for env var "TEST" does not exist.\nUnable to offer any suggestions.', 24 | }, 25 | ]); 26 | }); 27 | 28 | it("should return suggestions if type is close to a known type", () => { 29 | expect( 30 | validator(null, [{ name: "TEST", type: "int" }], {}) 31 | ).to.deep.equal([ 32 | { 33 | error$: 34 | 'The type int for env var "TEST" does not exist.\nMaybe you meant Int?', 35 | }, 36 | ]); 37 | 38 | expect( 39 | validator(null, [{ name: "TEST", type: "bint" }], {}) 40 | ).to.deep.equal([ 41 | { 42 | error$: 43 | 'The type bint for env var "TEST" does not exist.\nMaybe you meant Int?', 44 | }, 45 | ]); 46 | 47 | expect( 48 | validator(null, [{ name: "TEST", type: "sloat" }], {}) 49 | ).to.deep.equal([ 50 | { 51 | error$: 52 | 'The type sloat for env var "TEST" does not exist.\nMaybe you meant Float?', 53 | }, 54 | ]); 55 | }); 56 | 57 | it("should return as many results as definitions", () => { 58 | expect( 59 | validator(null, [{ name: "TEST" }, { name: "TEST2" }], {}).length 60 | ).to.equal(2); 61 | }); 62 | }); 63 | 64 | describe("when optional", () => { 65 | it("should not return error if value is missing", () => { 66 | expect( 67 | validator(null, [{ name: "TEST", type: "String", default: "" }], {}) 68 | ).to.deep.equal([{ TEST: "" }]); 69 | }); 70 | 71 | it("should return error if type doesn't exist", () => { 72 | expect( 73 | validator(null, [{ name: "TEST", type: "Test" }], {}) 74 | ).to.deep.equal([ 75 | { 76 | error$: 77 | 'The type Test for env var "TEST" does not exist.\nUnable to offer any suggestions.', 78 | }, 79 | ]); 80 | }); 81 | 82 | it("should return as many results as definitions", () => { 83 | expect( 84 | validator( 85 | null, 86 | [{ name: "TEST", type: "String", default: "" }, { name: "TEST2" }], 87 | {} 88 | ).length 89 | ).to.equal(2); 90 | }); 91 | }); 92 | 93 | describe("custom", () => { 94 | it("should use custom validator", () => { 95 | expect( 96 | validator( 97 | { 98 | FortyTwo: () => 42, 99 | }, 100 | [{ name: "TEST", type: "FortyTwo", default: "1" }], 101 | {} 102 | ) 103 | ).to.deep.equal([{ TEST: 42 }]); 104 | }); 105 | 106 | it("should overwrite build-in validators", () => { 107 | expect( 108 | validator( 109 | { 110 | String: () => 42, 111 | }, 112 | [{ name: "TEST", type: "String", default: "" }], 113 | {} 114 | ) 115 | ).to.deep.equal([{ TEST: 42 }]); 116 | }); 117 | }); 118 | 119 | describe("built-in validator", () => { 120 | const typeTest = (type) => (value, def) => 121 | validator( 122 | {}, 123 | [ 124 | { 125 | name: "TEST", 126 | type, 127 | default: def, 128 | }, 129 | ], 130 | value !== undefined 131 | ? { 132 | TEST: value, 133 | } 134 | : {} 135 | )[0]; 136 | 137 | describe("String", () => { 138 | const String = typeTest("String"); 139 | 140 | it("should return value if value is provided", () => { 141 | (String("test", "") as Env).TEST.should.equal("test"); 142 | (String("test", undefined) as Env).TEST.should.equal("test"); 143 | (String("test", "def") as Env).TEST.should.equal("test"); 144 | }); 145 | 146 | it("should return default if no value is provided", () => { 147 | (String("", "") as Env).TEST.should.equal(""); 148 | (String(undefined, "") as Env).TEST.should.equal(""); 149 | (String("", "def") as Env).TEST.should.equal("def"); 150 | }); 151 | }); 152 | 153 | describe("Bool", () => { 154 | const Bool = typeTest("Bool"); 155 | 156 | it("should return value if value is provided and valid", () => { 157 | (Bool("false", "") as Env).TEST.should.equal(false); 158 | (Bool("false", "true") as Env).TEST.should.equal(false); 159 | (Bool("true", undefined) as Env).TEST.should.equal(true); 160 | (Bool("true", "false") as Env).TEST.should.equal(true); 161 | }); 162 | 163 | it("should return default if no value is provided", () => { 164 | expect((Bool("", "") as Env).TEST).to.equal(undefined); 165 | (Bool("", "true") as Env).TEST.should.equal(true); 166 | (Bool(undefined, "false") as Env).TEST.should.equal(false); 167 | }); 168 | 169 | it("should return undefined if neither value nor default is provided", () => { 170 | expect((Bool("", "") as Env).TEST).to.equal(undefined); 171 | expect((Bool("", undefined) as Env).TEST).to.equal(undefined); 172 | expect((Bool(undefined, "") as Env).TEST).to.equal(undefined); 173 | }); 174 | 175 | it("should throw if default is invalid", () => { 176 | const errorMsg = 177 | 'Default value for "TEST" is invalid: value is not a valid Bool'; 178 | Bool("true", "asd").error$.should.equal(errorMsg); 179 | Bool(undefined, "asd").error$.should.equal(errorMsg); 180 | Bool("", "1").error$.should.equal(errorMsg); 181 | }); 182 | 183 | it("should throw if value is invalid", () => { 184 | const errorMsg = 185 | 'Value for "TEST" is invalid: value is not a valid Bool'; 186 | Bool("asd", "").error$.should.equal(errorMsg); 187 | Bool("asd", "true").error$.should.equal(errorMsg); 188 | Bool("1", undefined).error$.should.equal(errorMsg); 189 | }); 190 | }); 191 | 192 | describe("Int", () => { 193 | const Int = typeTest("Int"); 194 | 195 | it("should return value if value is provided and valid", () => { 196 | (Int("1", "") as Env).TEST.should.equal(1); 197 | (Int("1", undefined) as Env).TEST.should.equal(1); 198 | (Int("0", "2") as Env).TEST.should.equal(0); 199 | (Int("-10000", "999") as Env).TEST.should.equal(-10000); 200 | }); 201 | 202 | it("should return default if no value is provided", () => { 203 | (Int("", "1") as Env).TEST.should.equal(1); 204 | (Int(undefined, "0") as Env).TEST.should.equal(0); 205 | (Int("", "-999") as Env).TEST.should.equal(-999); 206 | }); 207 | 208 | it("should return undefined if neither value nor default is provided", () => { 209 | expect((Int("", "") as Env).TEST).to.equal(undefined); 210 | expect((Int("", undefined) as Env).TEST).to.equal(undefined); 211 | expect((Int(undefined, "") as Env).TEST).to.equal(undefined); 212 | }); 213 | 214 | it("should throw if default is invalid", () => { 215 | const errorMsg = 216 | 'Default value for "TEST" is invalid: value is not a valid Int'; 217 | Int("1", "asd").error$.should.equal(errorMsg); 218 | Int("", "1.1").error$.should.equal(errorMsg); 219 | Int(undefined, "true").error$.should.equal(errorMsg); 220 | }); 221 | 222 | it("should throw if value is invalid", () => { 223 | const errorMsg = 224 | 'Value for "TEST" is invalid: value is not a valid Int'; 225 | Int("asd", "1").error$.should.equal(errorMsg); 226 | Int("1.1", "").error$.should.equal(errorMsg); 227 | Int("true", undefined).error$.should.equal(errorMsg); 228 | }); 229 | }); 230 | 231 | describe("Float", () => { 232 | const Float = typeTest("Float"); 233 | 234 | it("should return value if value is provided and valid", () => { 235 | (Float("1.1", "") as Env).TEST.should.equal(1.1); 236 | (Float(".1", undefined) as Env).TEST.should.equal(0.1); 237 | (Float("0", "2") as Env).TEST.should.equal(0); 238 | (Float("-10000.3", "999.9") as Env).TEST.should.equal(-10000.3); 239 | }); 240 | 241 | it("should return default if no value is provided", () => { 242 | (Float("", "1.1") as Env).TEST.should.equal(1.1); 243 | (Float(undefined, ".1") as Env).TEST.should.equal(0.1); 244 | (Float("", "0") as Env).TEST.should.equal(0); 245 | (Float("", "-10000.3") as Env).TEST.should.equal(-10000.3); 246 | }); 247 | 248 | it("should return undefined if neither value nor default is provided", () => { 249 | expect((Float("", "") as Env).TEST).to.equal(undefined); 250 | expect((Float("", undefined) as Env).TEST).to.equal(undefined); 251 | expect((Float(undefined, "") as Env).TEST).to.equal(undefined); 252 | }); 253 | 254 | it("should throw if default is invalid", () => { 255 | const errorMsg = 256 | 'Default value for "TEST" is invalid: value is not a valid Float'; 257 | Float("1", "asd").error$.should.equal(errorMsg); 258 | Float("", "1.1.1").error$.should.equal(errorMsg); 259 | Float(undefined, "true").error$.should.equal(errorMsg); 260 | }); 261 | 262 | it("should throw if value is invalid", () => { 263 | const errorMsg = 264 | 'Value for "TEST" is invalid: value is not a valid Float'; 265 | Float("asd", "1").error$.should.equal(errorMsg); 266 | Float("1.1.1", "").error$.should.equal(errorMsg); 267 | Float("true", undefined).error$.should.equal(errorMsg); 268 | }); 269 | }); 270 | 271 | describe("Json", () => { 272 | const Json = typeTest("Json"); 273 | 274 | it("should return value if value is provided and valid", () => { 275 | (Json('{"asd":1}', "") as Env).TEST.should.deep.equal({ asd: 1 }); 276 | (Json('"asd"', undefined) as Env).TEST.should.equal("asd"); 277 | (Json('{"asd":1}', "{}") as Env).TEST.should.deep.equal({ asd: 1 }); 278 | expect((Json("null", "") as Env).TEST).to.equal(null); 279 | }); 280 | 281 | it("should return default if no value is provided", () => { 282 | (Json("", '{"asd":1}') as Env).TEST.should.deep.equal({ asd: 1 }); 283 | (Json(undefined, '"asd"') as Env).TEST.should.equal("asd"); 284 | expect((Json("", "null") as Env).TEST).to.equal(null); 285 | }); 286 | 287 | it("should return undefined if neither value nor default is provided", () => { 288 | expect((Json("", "") as Env).TEST).to.equal(undefined); 289 | expect((Json("", undefined) as Env).TEST).to.equal(undefined); 290 | expect((Json(undefined, "") as Env).TEST).to.equal(undefined); 291 | }); 292 | 293 | it("should throw if default is invalid", () => { 294 | const errorMsg = 295 | 'Default value for "TEST" is invalid: value is not a valid Json'; 296 | Json("{}", "asd").error$.should.equal(errorMsg); 297 | Json(undefined, "{").error$.should.equal(errorMsg); 298 | }); 299 | 300 | it("should throw if value is invalid", () => { 301 | const errorMsg = 302 | 'Value for "TEST" is invalid: value is not a valid Json'; 303 | Json("asd", "").error$.should.equal(errorMsg); 304 | Json("{", undefined).error$.should.equal(errorMsg); 305 | }); 306 | }); 307 | }); 308 | }); 309 | --------------------------------------------------------------------------------