Skip to content

Commit 7de638e

Browse files
committed
Fix Nullable schema for ReScript API
1 parent f496aac commit 7de638e

21 files changed

+180
-49
lines changed

IDEAS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
### rc.6
66

77
- Fix union bug with `S.unknown` (https://github.com/DZakh/sury/issues/121)
8+
- (rescript) Renamed `S.nullish` back to `S.nullable`
9+
- (rescript) Added `S.nullableAsOption` to replace `S.nullable` from V9
10+
- (ppx) Returned back `@s.nullable` attribute
11+
- (ppx) Added support for `Js.nullable` type
812

913
### Final release fixes
1014

11-
- Return `@s.nullish` to ppx
12-
- (rescript) Rename `S.nullish` to `S.nullable`, `S.nullableAsOption`
1315
- Add title to meta
14-
1516
- Add `S.env` to support coercion for union items separately. Like `rescript-envsafe` used to do with `preprocess`
1617
- Make `S.record` accept two args
1718
- Update docs

docs/rescript-usage.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
- [`Option.getOr`](#optiongetor)
2121
- [`Option.getOrWith`](#optiongetorwith)
2222
- [`null`](#null)
23-
- [`nullish`](#nullish)
23+
- [`nullable`](#nullable)
24+
- [`nullableAsOption`](#nullableasoption)
2425
- [`unit`](#unit)
26+
- [`nullAsUnit`](#nullasunit)
2527
- [`literal`](#literal)
2628
- [`object`](#object)
2729
- [Transform object field names](#transform-object-field-names)
@@ -418,12 +420,12 @@ The `S.null` schema represents a data of a specific type that might be null.
418420

419421
> 🧠 Since `S.null` transforms value into `option` type, you can use `Option.getOr`/`Option.getOrWith` for it as well.
420422
421-
### **`nullish`**
423+
### **`nullable`**
422424

423425
`S.t<'value> => S.t<Nullable.t<'value>>`
424426

425427
```rescript
426-
let schema = S.nullish(S.string)
428+
let schema = S.nullable(S.string)
427429
428430
"Hello World!"->S.parseOrThrow(schema)
429431
// Some("Hello World!")
@@ -433,7 +435,13 @@ let schema = S.nullish(S.string)
433435
// Undefined
434436
```
435437

436-
The `S.nullish` schema represents a data of a specific type that might be null or undefined.
438+
The `S.nullable` schema represents a data of `Nullable.t` that might be null or undefined.
439+
440+
### **`nullableAsOption`**
441+
442+
`S.t<'value> => S.t<option<'value>>`
443+
444+
The same as `S.nullable`, but returns `option` type instead of `Nullable.t`. When serializing, it will return `undefined` for `None` values.
437445

438446
### **`unit`**
439447

packages/e2e/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"res": "rescript -w",
88
"test": "rescript && ava",
99
"ppx:install": "node ../sury-ppx/install.cjs",
10+
"ppx:build": "cd ../sury-ppx/src && dune build",
1011
"benchmark": "node ./src/benchmark/Benchmark.res.mjs",
1112
"benchmark:comparison": "node ./src/benchmark/comparison.js"
1213
},

packages/e2e/src/ppx/Ppx_Example_test.res

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,20 @@ test("@s.null with @s.default", t => {
7979
t->assertEqualSchemas(nullWithDefaultSchema, S.null(S.string)->S.Option.getOr("Unknown"))
8080
})
8181

82-
// FIXME: IT DOESN'T WORK. Fix before V10
83-
// @schema
84-
// type nullish = @s.nullish option<string>
85-
// test("@s.nullish", t => {
86-
// t->assertEqualSchemas(nullishSchema, S.nullish(S.string))
87-
// })
82+
@schema
83+
type nullable = @s.nullable option<string>
84+
test("@s.nullable", t => {
85+
t->assertEqualSchemas(nullableSchema, S.nullableAsOption(S.string))
86+
})
8887

89-
// @schema
90-
// type nullableWithDefault = @s.nullish @s.default("Unknown") string
91-
// test("@s.nullish with @s.default", t => {
92-
// t->assertEqualSchemas(nullableWithDefaultSchema, S.nullish(S.string)->S.Option.getOr("Unknown"))
93-
// })
88+
@schema
89+
type nullableWithDefault = @s.nullable @s.default("Unknown") string
90+
test("@s.nullable with @s.default", t => {
91+
t->assertEqualSchemas(
92+
nullableWithDefaultSchema,
93+
S.nullableAsOption(S.string)->S.Option.getOr("Unknown"),
94+
)
95+
})
9496

9597
@schema
9698
type deprecated = @s.meta({description: "Will be removed in APIv2", deprecated: true}) string

packages/e2e/src/ppx/Ppx_Example_test.res.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ Ava("@s.null with @s.default", (function (t) {
7878
U.assertEqualSchemas(t, nullWithDefaultSchema, S.$$Option.getOr(S.$$null(S.string), "Unknown"), undefined);
7979
}));
8080

81+
var nullableSchema = S.nullableAsOption(S.string);
82+
83+
Ava("@s.nullable", (function (t) {
84+
U.assertEqualSchemas(t, nullableSchema, S.nullableAsOption(S.string), undefined);
85+
}));
86+
87+
var nullableWithDefaultSchema = S.$$Option.getOr(S.nullableAsOption(S.string), "Unknown");
88+
89+
Ava("@s.nullable with @s.default", (function (t) {
90+
U.assertEqualSchemas(t, nullableWithDefaultSchema, S.$$Option.getOr(S.nullableAsOption(S.string), "Unknown"), undefined);
91+
}));
92+
8193
var deprecatedSchema = S.meta(S.string, {
8294
description: "Will be removed in APIv2",
8395
deprecated: true
@@ -108,6 +120,8 @@ export {
108120
defaultWithSchema ,
109121
nullSchema ,
110122
nullWithDefaultSchema ,
123+
nullableSchema ,
124+
nullableWithDefaultSchema ,
111125
deprecatedSchema ,
112126
describeSchema ,
113127
}

packages/e2e/src/ppx/Ppx_Primitive_test.res

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ test("Option of string schema", t => {
5050
t->assertEqualSchemas(myOptionOfStringSchema, S.option(S.string))
5151
})
5252

53+
@schema
54+
type myNullableOfString = Nullable.t<string>
55+
test("Nullable of string schema", t => {
56+
t->assertEqualSchemas(myNullableOfStringSchema, S.nullable(S.string))
57+
})
58+
5359
@schema
5460
type myArrayOfString = array<string>
5561
test("Array of string schema", t => {

packages/e2e/src/ppx/Ppx_Primitive_test.res.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ Ava("Option of string schema", (function (t) {
3838
U.assertEqualSchemas(t, myOptionOfStringSchema, S.option(S.string), undefined);
3939
}));
4040

41+
var myNullableOfStringSchema = S.nullable(S.string);
42+
43+
Ava("Nullable of string schema", (function (t) {
44+
U.assertEqualSchemas(t, myNullableOfStringSchema, S.nullable(S.string), undefined);
45+
}));
46+
4147
var myArrayOfStringSchema = S.array(S.string);
4248

4349
Ava("Array of string schema", (function (t) {
@@ -174,6 +180,7 @@ export {
174180
myUnknownSchema ,
175181
myNeverSchema ,
176182
myOptionOfStringSchema ,
183+
myNullableOfStringSchema ,
177184
myArrayOfStringSchema ,
178185
myListOfStringSchema ,
179186
myDictOfStringSchema ,

packages/sury-ppx/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ type t = @s.null option<string>
143143
let schema = S.null(S.string)
144144
```
145145

146+
### `@s.nullable`
147+
148+
**Applies to**: option type expressions
149+
150+
Tells to use `S.nullableAsOption` for the option schema constructor.
151+
152+
```rescript
153+
@schema
154+
type t = @s.nullable option<string>
155+
156+
// Generated by PPX ⬇️
157+
let schema = S.nullableAsOption(S.string)
158+
```
159+
146160
### `@s.default('value)`
147161

148162
**Applies to**: type expressions

packages/sury-ppx/src/ppx/Structure.ml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ let rec generateConstrSchemaExpression {Location.txt = identifier; loc}
2929
| Lident "null", [item_type] ->
3030
[%expr S.null [%e generateCoreTypeSchemaExpression item_type]]
3131
| Ldot (Ldot (Lident "Js", "Nullable"), "t"), [item_type]
32-
| Ldot (Lident "Nullable", "t"), [item_type] ->
33-
[%expr S.nullish [%e generateCoreTypeSchemaExpression item_type]]
32+
| Ldot (Lident "Nullable", "t"), [item_type]
33+
| Ldot (Lident "Js", "nullable"), [item_type] ->
34+
[%expr S.nullable [%e generateCoreTypeSchemaExpression item_type]]
3435
| Lident "dict", [item_type]
3536
| Ldot (Ldot (Lident "Js", "Dict"), "t"), [item_type]
3637
| Ldot (Lident "Dict", "t"), [item_type] ->
@@ -143,11 +144,16 @@ and generateCoreTypeSchemaExpression core_type =
143144
let customSchemaExpression = getAttributeByName ptyp_attributes "s.matches" in
144145
let option_factory_expression =
145146
match
146-
( getAttributeByName ptyp_attributes "s.null" )
147+
( getAttributeByName ptyp_attributes "s.null",
148+
getAttributeByName ptyp_attributes "s.nullable" )
147149
with
148-
| Ok None -> [%expr S.option]
149-
| Ok (Some _) -> [%expr S.null]
150-
| Error s -> fail ptyp_loc s
150+
| Ok None, Ok None -> [%expr S.option]
151+
| Ok (Some _), Ok None -> [%expr S.null]
152+
| Ok None, Ok (Some _) -> [%expr S.nullableAsOption]
153+
| Ok (Some _), Ok (Some _) ->
154+
fail ptyp_loc
155+
"Attributes @s.null and @s.nullable are not supported at the same time"
156+
| _, Error s | Error s, _ -> fail ptyp_loc s
151157
in
152158
let schema_expression =
153159
match customSchemaExpression with
@@ -183,7 +189,7 @@ and generateCoreTypeSchemaExpression core_type =
183189
in
184190
let handle_attribute schema_expr ({attr_name = {Location.txt}} as attribute) =
185191
match txt with
186-
| "s.matches" | "s.null" -> schema_expr (* handled above *)
192+
| "s.matches" | "s.null" | "s.nullable" -> schema_expr (* handled above *)
187193
| "s.default" ->
188194
let default_value = getExpressionFromPayload attribute in
189195
[%expr

packages/sury/output-test.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
(i) => {
2-
if (i instanceof e[0]) {
3-
} else if (typeof i === "object" && i) {
4-
let v0 = i["foo"];
5-
if (typeof v0 !== "string") {
6-
e[1](v0);
7-
}
8-
i = [v0];
9-
} else {
10-
e[2](i);
2+
if (i === null) {
3+
i = void 0;
4+
} else if (!(typeof i === "boolean" || i === void 0)) {
5+
e[0](i);
116
}
127
return i;
138
};

packages/sury/scripts/pack/Pack.res

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ let filesMapping = [
126126
("unknown", "S.unknown"),
127127
("optional", "S.js_optional"),
128128
("nullable", "S.js_nullable"),
129-
("nullish", "S.nullish"),
129+
("nullish", "S.nullable"),
130130
("array", "S.array"),
131131
("instance", "S.instance"),
132132
("unnest", "S.unnest"),

packages/sury/scripts/pack/Pack.res.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ var filesMapping = [
103103
],
104104
[
105105
"nullish",
106-
"S.nullish"
106+
"S.nullable"
107107
],
108108
[
109109
"array",

packages/sury/src/S.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const never = S.never
1212
export const unknown = S.unknown
1313
export const optional = S.js_optional
1414
export const nullable = S.js_nullable
15-
export const nullish = S.nullish
15+
export const nullish = S.nullable
1616
export const array = S.array
1717
export const instance = S.instance
1818
export const unnest = S.unnest

packages/sury/src/S.res.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ var option = Sury.option;
4646

4747
var $$null = Sury.$$null;
4848

49-
var nullish = Sury.nullish;
49+
var nullable = Sury.nullable;
50+
51+
var nullableAsOption = Sury.nullableAsOption;
5052

5153
var jsonString = Sury.jsonString;
5254

@@ -201,7 +203,8 @@ export {
201203
dict ,
202204
option ,
203205
$$null ,
204-
nullish ,
206+
nullable ,
207+
nullableAsOption ,
205208
jsonString ,
206209
union ,
207210
$$enum ,

packages/sury/src/S.resi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ let instance: unknown => t<unknown>
266266
let dict: t<'value> => t<dict<'value>>
267267
let option: t<'value> => t<option<'value>>
268268
let null: t<'value> => t<option<'value>>
269-
let nullish: t<'value> => t<Js.nullable<'value>>
269+
let nullable: t<'value> => t<Js.nullable<'value>>
270+
let nullableAsOption: t<'value> => t<option<'value>>
270271
let jsonString: (t<'value>, ~space: int=?) => t<'value>
271272
let union: array<t<'value>> => t<'value>
272273
let enum: array<'value> => t<'value>

packages/sury/src/Sury.res

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4831,10 +4831,14 @@ let trim = schema => {
48314831
schema->transform(_ => {parser: transformer, serializer: transformer})
48324832
}
48334833

4834-
let nullish = schema => {
4834+
let nullable = schema => {
48354835
Union.factory([schema->toUnknown, unit->toUnknown, Literal.null->fromInternal])
48364836
}
48374837

4838+
let nullableAsOption = schema => {
4839+
Union.factory([schema->toUnknown, unit->toUnknown, nullAsUnit->toUnknown])
4840+
}
4841+
48384842
// =============
48394843
// JS/TS API
48404844
// =============

packages/sury/src/Sury.res.mjs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3652,14 +3652,22 @@ function trim(schema) {
36523652
}));
36533653
}
36543654

3655-
function nullish(schema) {
3655+
function nullable(schema) {
36563656
return factory([
36573657
schema,
36583658
unit,
36593659
$$null
36603660
]);
36613661
}
36623662

3663+
function nullableAsOption(schema) {
3664+
return factory([
3665+
schema,
3666+
unit,
3667+
nullAsUnit
3668+
]);
3669+
}
3670+
36633671
function js_union(values) {
36643672
return factory(values.map(definitionToSchema));
36653673
}
@@ -4555,7 +4563,8 @@ export {
45554563
dict ,
45564564
option ,
45574565
$$null$1 as $$null,
4558-
nullish ,
4566+
nullable ,
4567+
nullableAsOption ,
45594568
jsonString ,
45604569
union ,
45614570
$$enum ,

packages/sury/src/Sury.resi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ let instance: unknown => t<unknown>
266266
let dict: t<'value> => t<dict<'value>>
267267
let option: t<'value> => t<option<'value>>
268268
let null: t<'value> => t<option<'value>>
269-
let nullish: t<'value> => t<Js.nullable<'value>>
269+
let nullable: t<'value> => t<Js.nullable<'value>>
270+
let nullableAsOption: t<'value> => t<option<'value>>
270271
let jsonString: (t<'value>, ~space: int=?) => t<'value>
271272
let union: array<t<'value>> => t<'value>
272273
let enum: array<'value> => t<'value>

0 commit comments

Comments
 (0)