Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#71] Implement custom accessors #72

Merged
merged 18 commits into from
Sep 5, 2019
Merged
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: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ node_js:
- "10"
- "9"
- "8"
- "7"
- "6"

after_success:
- npm run coveralls
103 changes: 96 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@

</div>

Verification, sanatization, and type coercion for environment variables in
Verification, sanitization, and type coercion for environment variables in
Node.js. Particularly useful in TypeScript environments.

## Install
**Note:** env-var requires Node version 8 or later.

### npm
```
Expand Down Expand Up @@ -73,13 +74,13 @@ complex to understand as [demonstrated here](https://gist.github.com/evanshortis

* module (env-var)
* [EnvVarError()](#envvarerror)
* [from()](#fromvalues)
* [from()](#fromvalues-extraaccessors)
* [get()](#getvarname-default)
* [variable](#variable)
* [required()](#requiredisrequired--true)
* [covertFromBase64()](#convertfrombase64)
* [asArray()](#asarraydelimiter-string)
* [asBoolStrict()](#asBoolStrict)
* [asBoolStrict()](#asboolstrict)
* [asBool()](#asbool)
* [asPortNumer()](#asportnumber)
* [asEnum()](#asenumvalidvalues-string)
Expand Down Expand Up @@ -119,7 +120,7 @@ try {
}
```

### from(values)
### from(values, extraAccessors)
This function is useful if you're not in a typical Node.js environment, or for
testing. It allows you to generate an env-var instance that reads from the
given `values` instead of the default `process.env`.
Expand All @@ -133,6 +134,94 @@ const env = require('env-var').from({
const apiUrl = mockedEnv.get('API_BASE_URL').asUrlString()
```

#### extraAccessors
When calling `from()` you can also pass an optional parameter containing
additional accessors that will be attached to any variables gotten by that
env-var instance.

Accessor functions must accept at least one argument:

- `{*} value`: The value that the accessor should process.

**Important:** Do not assume that `value` is a string!

Example:
```js
const { from } = require('env-var')

// Environment variable that we will use for this example:
process.env.ADMIN = '[email protected]'

// Add an accessor named 'checkEmail' that verifies that the value is a
// valid-looking email address.
const env = from(process.env, {
checkEmail: (value) => {
const split = String(value).split('@')

// Validating email addresses is hard.
if (split.length !== 2) {
throw new Error('must contain exactly one "@"')
}

return value
}
})

// We specified 'checkEmail' as the name for the accessor above, so now
// we can call `checkEmail()` like any other accessor.
let validEmail = env.get('ADMIN').checkEmail()
```

The accessor function may accept additional arguments if desired; these must be
provided explicitly when the accessor is invoked.

For example, we can modify the `checkEmail()` accessor from above so that it
optionally verifies the domain of the email address:
```js
const { from } = require('env-var')

// Environment variable that we will use for this example:
process.env.ADMIN = '[email protected]'

// Add an accessor named 'checkEmail' that verifies that the value is a
// valid-looking email address.
//
// Note that the accessor function also accepts an optional second
// parameter `requiredDomain` which can be provided when the accessor is
// invoked (see below).
const env = from(process.env, {
checkEmail: (value, requiredDomain) => {
const split = String(value).split('@')

// Validating email addresses is hard.
if (split.length !== 2) {
throw new Error('must contain exactly one "@"')
}

if (requiredDomain && (split[1] !== requiredDomain)) {
throw new Error(`must end with @${requiredDomain}`)
}

return value
}
})

// We specified 'checkEmail' as the name for the accessor above, so now
// we can call `checkEmail()` like any other accessor.
//
// `env-var` will provide the first argument for the accessor function
// (`value`), but we declared a second argument `requiredDomain`, which
// we can provide when we invoke the accessor.

// Calling the accessor without additional parameters accepts an email
// address with any domain.
let validEmail = env.get('ADMIN').checkEmail()

// If we specify a parameter, then the email address must end with the
// domain we specified.
let invalidEmail = env.get('ADMIN').checkEmail('github.com')
```

### get([varname, [default]])
You can call this function 3 different ways:

Expand Down Expand Up @@ -314,9 +403,9 @@ const enumVal = env.get('ENVIRONMENT').asEnum(['dev', 'test', 'live'])
Contributions are welcomed. If you'd like to discuss an idea open an issue, or a
PR with an initial implementation.

If you want to add a new type it's easy. Add a file to `lib/accessors`,
with the name of the type e.g add a file named `number-zero.js` into that folder
and populate it with code following this structure:
If you want to add a new global accessor, it's easy. Add a file to
`lib/accessors`, with the name of the type e.g add a file named `number-zero.js`
into that folder and populate it with code following this structure:

```js
/**
Expand Down
8 changes: 5 additions & 3 deletions env-var.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ const variable = require('./lib/variable')
* Returns an "env-var" instance that reads from the given container of values.
* By default, we export an instance that reads from process.env
* @param {Object} container target container to read values from
* @param {Object} extraAccessors additional accessors to attach to the
* resulting object
* @return {Object} a new module instance
*/
const from = (container) => {
const from = (container, extraAccessors) => {
return {
from: from,

/**
* This is the Error class used to generate exceptions. Can be used to identify
* exceptions adn handle them appropriatly.
* exceptions and handle them appropriately.
*/
EnvVarError: require('./lib/env-error'),

Expand All @@ -29,7 +31,7 @@ const from = (container) => {
return container
}

return variable(container, variableName, defaultValue)
return variable(container, variableName, defaultValue, extraAccessors || {})
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion lib/variable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ const base64Regex = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-
/**
* Returns an Object that contains functions to read and specify the format of
* the variable you wish to have returned
* @param {Object} container Encapsulated container (e.g., `process.env`).
* @param {String} varName Name of the requested property from `container`.
* @param {*} defValue Default value to return if `varName` is invalid.
* @param {Object} extraAccessors Extra accessors to install.
* @return {Object}
*/
module.exports = function getVariableAccessors (container, varName, defValue) {
module.exports = function getVariableAccessors (container, varName, defValue, extraAccessors) {
let isBase64 = false

/**
Expand Down Expand Up @@ -111,5 +115,10 @@ module.exports = function getVariableAccessors (container, varName, defValue) {
}
}

// Attach extra accessors, if provided.
Object.entries(extraAccessors).forEach(([name, accessor]) => {
accessors[name] = generateAccessor(accessor)
})

return accessors
}
41 changes: 41 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,5 +505,46 @@ describe('env-var', function () {
expect(fromMod.get()).to.have.property('A_BOOL', 'true')
expect(fromMod.get()).to.have.property('A_STRING', 'blah')
})

describe(':extraAccessors', function () {
it('should add custom accessors to subsequent gotten values', function () {
const fromMod = mod.from({ STRING: 'Hello, world!' }, {
asShout: function (value) {
return value.toUpperCase()
}
})

var gotten = fromMod.get('STRING')

expect(gotten).to.have.property('asShout')
expect(gotten.asShout()).to.equal('HELLO, WORLD!')
})

it('should allow overriding existing accessors', function () {
const fromMod = mod.from({ STRING: 'Hello, world!' }, {
asString: function (value) {
// https://stackoverflow.com/a/959004
return value.split('').reverse().join('')
}
})

expect(fromMod.get('STRING').asString()).to.equal('!dlrow ,olleH')
})

it('should not attach accessors to other env instances', function () {
const fromMod = mod.from({ STRING: 'Hello, world!' }, {
asNull: function (value) {
return null
}
})

var otherMod = mod.from({
STRING: 'Hola, mundo!'
})

expect(fromMod.get('STRING')).to.have.property('asNull')
expect(otherMod.get('STRING')).not.to.have.property('asNull')
})
})
})
})