Skip to content

Commit fc5645a

Browse files
Add type deprecated (ianstormtaylor#534)
* Add type deprecated * Improve test description * Reformat * Validate passed struct * Clean up * Adapt code for master and improve tests * Add doc * Improve description * Pass custom logging function with value and ctx * Do not log if value is already unused * Update documentation * Adapt comment * Adapt changes for branch main * Improve comment * Update index.ts * Update utilities.md * Update utilities.ts * Update .eslintrc Co-authored-by: Ian Storm Taylor <[email protected]>
1 parent 69e2188 commit fc5645a

File tree

11 files changed

+171
-6
lines changed

11 files changed

+171
-6
lines changed

.eslintrc

-4
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@
104104
"radix": "error",
105105
"spaced-comment": ["error", "always", { "exceptions": ["-"] }],
106106
"use-isnan": "error",
107-
"valid-jsdoc": [
108-
"error",
109-
{ "prefer": { "return": "returns" }, "requireReturn": false }
110-
],
111107
"valid-typeof": "error",
112108
"yield-star-spacing": ["error", "after"],
113109
"yoda": ["error", "never"]

docs/reference/utilities.md

+18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@ assign(object({ id: string() }), object({ name: string() }))
1717

1818
`assign` creates a new struct by mixing the properties of existing object structs, similar to JavaScript's native [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).
1919

20+
### `deprecated`
21+
22+
```ts
23+
object({
24+
id: number(),
25+
full_name: string(),
26+
name: deprecated(string(), (value, ctx) => {
27+
console.warn(`${ctx.path} is deprecated, but value was '${value}'. Please use 'full_name' instead.`)
28+
}),
29+
})
30+
```
31+
32+
```ts
33+
{ id: 1, name: 'Jane' }
34+
```
35+
36+
`deprecated` structs validate that a value matches a specific struct, or that it is `undefined`. But in addition, when the value is not `undefined`, it will call the `log` function you pass in so you can warn users that they're using a deprecated API.
37+
2038
### `dynamic`
2139

2240
```ts

src/structs/utilities.ts

+23
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,29 @@ export function define<T>(name: string, validator: Validator): Struct<T, null> {
6666
return new Struct({ type: name, schema: null, validator })
6767
}
6868

69+
/**
70+
* Create a new struct based on an existing struct, but the value is allowed to
71+
* be `undefined`. `log` will be called if the value is not `undefined`.
72+
*/
73+
74+
export function deprecated<T>(
75+
struct: Struct<T>,
76+
log: (value: unknown, ctx: Context) => void
77+
): Struct<T> {
78+
return new Struct({
79+
...struct,
80+
refiner: (value, ctx) => value === undefined || struct.refiner(value, ctx),
81+
validator(value, ctx) {
82+
if (value === undefined) {
83+
return true
84+
} else {
85+
log(value, ctx)
86+
return struct.validator(value, ctx)
87+
}
88+
},
89+
})
90+
}
91+
6992
/**
7093
* Create a struct with dynamic validation logic.
7194
*

test/index.ts

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import assert from 'assert'
1+
import assert, { CallTracker } from 'assert'
22
import fs from 'fs'
33
import { pick } from 'lodash'
44
import { basename, extname, resolve } from 'path'
5-
import { assert as assertValue, create as createValue, StructError } from '..'
5+
import {
6+
any,
7+
assert as assertValue,
8+
Context,
9+
create as createValue,
10+
deprecated,
11+
StructError,
12+
} from '..'
613

714
describe('superstruct', () => {
815
describe('api', () => {
@@ -83,10 +90,40 @@ describe('superstruct', () => {
8390
})
8491
}
8592
})
93+
94+
describe('deprecated', () => {
95+
it('does not log deprecated type if value is undefined', () => {
96+
const tracker = new CallTracker()
97+
const logSpy = buildSpyWithZeroCalls(tracker)
98+
assertValue(undefined, deprecated(any(), logSpy))
99+
tracker.verify()
100+
})
101+
102+
it('logs deprecated type to passed function if value is present', () => {
103+
const tracker = new CallTracker()
104+
const fakeLog = (value: unknown, ctx: Context) => {}
105+
const logSpy = tracker.calls(fakeLog, 1)
106+
assertValue('present', deprecated(any(), logSpy))
107+
tracker.verify()
108+
})
109+
})
86110
})
87111

88112
/**
89113
* A helper for testing type signatures.
90114
*/
91115

92116
export function test<T>(fn: (x: unknown) => T) {}
117+
118+
/**
119+
* This emulates `tracker.calls(0)`.
120+
*
121+
* `CallTracker.calls` doesn't support passing `0`, therefore we expect it
122+
* to be called once which is our call in this test. This proves that
123+
* the following action didn't call it.
124+
*/
125+
function buildSpyWithZeroCalls(tracker: CallTracker) {
126+
const logSpy = tracker.calls(1)
127+
logSpy()
128+
return logSpy
129+
}

test/typings/deprecated.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { assert, object, deprecated, any, Context } from '../..'
2+
import { test } from '..'
3+
4+
test<unknown>((x) => {
5+
const log = (value: unknown, ctx: Context) => {}
6+
assert(x, deprecated(any(), log))
7+
return x
8+
})
9+
10+
test<{ a?: unknown }>((x) => {
11+
const log = (value: unknown, ctx: Context) => {}
12+
assert(x, object({ a: deprecated(any(), log) }))
13+
return x
14+
})
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { deprecated, string } from '../../..'
2+
3+
export const Struct = deprecated(string(), () => {})
4+
5+
export const data = null
6+
7+
export const failures = [
8+
{
9+
value: null,
10+
type: 'string',
11+
refinement: undefined,
12+
path: [],
13+
branch: [data],
14+
},
15+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { deprecated, number, object } from '../../..'
2+
3+
export const Struct = object({
4+
deprecatedKey: deprecated(number(), () => {}),
5+
})
6+
7+
export const data = {
8+
deprecatedKey: '42',
9+
}
10+
11+
export const failures = [
12+
{
13+
value: '42',
14+
type: 'number',
15+
refinement: undefined,
16+
path: ['deprecatedKey'],
17+
branch: [data, data.deprecatedKey],
18+
},
19+
]

test/validation/deprecated/invalid.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { deprecated, number } from '../../..'
2+
3+
export const Struct = deprecated(number(), () => {})
4+
5+
export const data = '42'
6+
7+
export const failures = [
8+
{
9+
value: '42',
10+
type: 'number',
11+
refinement: undefined,
12+
path: [],
13+
branch: [data],
14+
},
15+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { type, number, deprecated, any } from '../../..'
2+
3+
export const Struct = type({
4+
name: deprecated(any(), () => {}),
5+
age: number(),
6+
})
7+
8+
export const data = {
9+
age: 42,
10+
}
11+
12+
export const output = {
13+
age: 42,
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { deprecated, number } from '../../..'
2+
3+
export const Struct = deprecated(number(), () => {})
4+
5+
export const data = undefined
6+
7+
export const output = undefined

test/validation/deprecated/valid.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { deprecated, number } from '../../..'
2+
3+
export const Struct = deprecated(number(), () => {})
4+
5+
export const data = 42
6+
7+
export const output = 42

0 commit comments

Comments
 (0)