Skip to content

Commit 6620c12

Browse files
fix(checkbox): add support for the checkbox type (#28)
I'm adding support for `checkbox` type as that wasn't supported previously, while it's used in Aggregators. As it's a legacy type (we have `boolean`, and checkbox is the same just with a different visuals), we accept loosing the UI information during round-trip conversion. The direction to JSON Schema and Validation are important.
1 parent 46e48f3 commit 6620c12

File tree

7 files changed

+235
-11
lines changed

7 files changed

+235
-11
lines changed

package-lock.json

Lines changed: 2 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@makehq/forman-schema",
3-
"version": "1.8.1",
3+
"version": "1.8.2",
44
"description": "Forman Schema Tools",
55
"license": "MIT",
66
"author": "Make",

src/forman.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const FORMAN_TYPE_MAP: Readonly<Record<string, JSONSchema7['type']>> = {
9696
editor: 'string',
9797
number: 'number',
9898
boolean: 'boolean',
99+
checkbox: 'boolean',
99100
date: 'string',
100101
json: 'string',
101102
buffer: 'string',

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type FormanSchemaFieldType =
1515
| 'text'
1616
| 'number'
1717
| 'boolean'
18+
| 'checkbox'
1819
| 'date'
1920
| 'json'
2021
| 'buffer'

src/validator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const FORMAN_TYPE_MAP: Readonly<Record<string, string | undefined>> = {
9090
text: 'string',
9191
number: 'number',
9292
boolean: 'boolean',
93+
checkbox: 'boolean',
9394
date: 'string',
9495
json: 'string',
9596
buffer: 'string',

test/test.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,4 +563,128 @@ describe('Forman Schema', () => {
563563
],
564564
});
565565
});
566+
567+
describe('Checkbox Type Conversion', () => {
568+
it('should convert Forman checkbox to JSON Schema boolean', () => {
569+
const formanSchema: FormanSchemaField = {
570+
name: 'enabled',
571+
type: 'checkbox',
572+
};
573+
const jsonSchema = toJSONSchema(formanSchema);
574+
expect(jsonSchema).toEqual({
575+
type: 'boolean',
576+
});
577+
});
578+
579+
it('should preserve default value in checkbox conversion', () => {
580+
const formanSchema: FormanSchemaField = {
581+
name: 'enabled',
582+
type: 'checkbox',
583+
default: true,
584+
};
585+
const jsonSchema = toJSONSchema(formanSchema);
586+
expect(jsonSchema).toEqual({
587+
type: 'boolean',
588+
default: true,
589+
});
590+
});
591+
592+
it('should convert JSON Schema boolean to Forman boolean (not checkbox)', () => {
593+
const jsonSchema: JSONSchema7 = {
594+
type: 'object',
595+
properties: {
596+
enabled: { type: 'boolean' },
597+
},
598+
};
599+
const formanSchema = toFormanSchema(jsonSchema);
600+
expect(formanSchema.spec).toEqual([
601+
{
602+
name: 'enabled',
603+
type: 'boolean',
604+
required: false,
605+
},
606+
]);
607+
});
608+
609+
it('should handle checkbox roundtrip (checkbox -> JSON -> boolean)', () => {
610+
// Forman checkbox -> JSON Schema
611+
const originalForman: FormanSchemaField = {
612+
name: 'settings',
613+
type: 'collection',
614+
spec: [{ name: 'enabled', type: 'checkbox', default: true }],
615+
};
616+
const jsonSchema = toJSONSchema(originalForman);
617+
618+
// JSON Schema -> Forman (becomes boolean, not checkbox)
619+
const convertedForman = toFormanSchema(jsonSchema);
620+
621+
// UI type is lost in roundtrip, becomes boolean with preserved default
622+
expect(convertedForman.spec).toEqual([
623+
{
624+
name: 'enabled',
625+
type: 'boolean',
626+
required: false,
627+
default: true,
628+
},
629+
]);
630+
});
631+
632+
it('should convert checkbox with label and help text', () => {
633+
const formanSchema: FormanSchemaField = {
634+
name: 'notifications',
635+
type: 'checkbox',
636+
label: 'Enable Notifications',
637+
help: 'Toggle to receive notifications',
638+
};
639+
const jsonSchema = toJSONSchema(formanSchema);
640+
expect(jsonSchema).toEqual({
641+
type: 'boolean',
642+
title: 'Enable Notifications',
643+
description: 'Toggle to receive notifications',
644+
});
645+
});
646+
647+
it('should convert checkbox in nested collection', () => {
648+
const formanSchema: FormanSchemaField = {
649+
name: 'settings',
650+
type: 'collection',
651+
spec: [
652+
{ name: 'darkMode', type: 'checkbox' },
653+
{ name: 'notifications', type: 'checkbox', default: false },
654+
],
655+
};
656+
const jsonSchema = toJSONSchema(formanSchema);
657+
expect(jsonSchema).toEqual({
658+
type: 'object',
659+
properties: {
660+
darkMode: { type: 'boolean' },
661+
notifications: { type: 'boolean', default: false },
662+
},
663+
required: [],
664+
});
665+
});
666+
667+
it('should convert checkbox in array', () => {
668+
const formanSchema: FormanSchemaField = {
669+
name: 'items',
670+
type: 'array',
671+
spec: {
672+
name: 'item',
673+
type: 'collection',
674+
spec: [{ name: 'active', type: 'checkbox' }],
675+
},
676+
};
677+
const jsonSchema = toJSONSchema(formanSchema);
678+
expect(jsonSchema).toEqual({
679+
type: 'array',
680+
items: {
681+
type: 'object',
682+
properties: {
683+
active: { type: 'boolean' },
684+
},
685+
required: [],
686+
},
687+
});
688+
});
689+
});
566690
});

test/validator-comprehensive.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('Forman Schema Comprehensive Coverage', () => {
2525
url: 'https://example.com',
2626
uuid: '123e4567-e89b-12d3-a456-426614174000',
2727
any: 'any-value',
28+
checkbox: true,
2829
};
2930

3031
const formanSchema = [
@@ -48,6 +49,7 @@ describe('Forman Schema Comprehensive Coverage', () => {
4849
{ name: 'url', type: 'url' },
4950
{ name: 'uuid', type: 'uuid' },
5051
{ name: 'any', type: 'any' },
52+
{ name: 'checkbox', type: 'checkbox' },
5153
];
5254

5355
expect(await validateForman(formanValue, formanSchema)).toEqual({
@@ -674,4 +676,107 @@ describe('Forman Schema Comprehensive Coverage', () => {
674676
});
675677
});
676678
});
679+
680+
describe('Checkbox Type', () => {
681+
it('should validate checkbox with true value', async () => {
682+
const result = await validateForman(
683+
{ enabled: true },
684+
[{ name: 'enabled', type: 'checkbox' }],
685+
);
686+
expect(result).toEqual({ valid: true, errors: [] });
687+
});
688+
689+
it('should validate checkbox with false value', async () => {
690+
const result = await validateForman(
691+
{ disabled: false },
692+
[{ name: 'disabled', type: 'checkbox' }],
693+
);
694+
expect(result).toEqual({ valid: true, errors: [] });
695+
});
696+
697+
it('should reject non-boolean values for checkbox', async () => {
698+
const result = await validateForman(
699+
{ enabled: 'true' },
700+
[{ name: 'enabled', type: 'checkbox' }],
701+
);
702+
expect(result).toEqual({
703+
valid: false,
704+
errors: [{
705+
domain: 'default',
706+
path: 'enabled',
707+
message: "Expected type 'boolean', got type 'string'.",
708+
}],
709+
});
710+
});
711+
712+
it('should reject number values for checkbox', async () => {
713+
const result = await validateForman(
714+
{ enabled: 1 },
715+
[{ name: 'enabled', type: 'checkbox' }],
716+
);
717+
expect(result).toEqual({
718+
valid: false,
719+
errors: [{
720+
domain: 'default',
721+
path: 'enabled',
722+
message: "Expected type 'boolean', got type 'number'.",
723+
}],
724+
});
725+
});
726+
727+
it('should reject null for required checkbox', async () => {
728+
const result = await validateForman(
729+
{ enabled: null },
730+
[{ name: 'enabled', type: 'checkbox', required: true }],
731+
);
732+
expect(result).toEqual({
733+
valid: false,
734+
errors: [{
735+
domain: 'default',
736+
path: 'enabled',
737+
message: 'Field is mandatory.',
738+
}],
739+
});
740+
});
741+
742+
it('should allow null for optional checkbox', async () => {
743+
const result = await validateForman(
744+
{ enabled: null },
745+
[{ name: 'enabled', type: 'checkbox' }],
746+
);
747+
expect(result).toEqual({ valid: true, errors: [] });
748+
});
749+
750+
it('should support default value for checkbox', async () => {
751+
const result = await validateForman(
752+
{},
753+
[{ name: 'enabled', type: 'checkbox', default: true }],
754+
);
755+
expect(result).toEqual({ valid: true, errors: [] });
756+
});
757+
758+
it('should validate checkbox in nested objects', async () => {
759+
const result = await validateForman(
760+
{ settings: { notifications: true } },
761+
[{
762+
name: 'settings',
763+
type: 'collection',
764+
spec: [{ name: 'notifications', type: 'checkbox' }],
765+
}],
766+
);
767+
expect(result).toEqual({ valid: true, errors: [] });
768+
});
769+
770+
it('should validate checkbox in arrays', async () => {
771+
const result = await validateForman(
772+
{ items: [{ active: true }, { active: false }] },
773+
[{
774+
name: 'items',
775+
type: 'array',
776+
spec: { type: 'collection', spec: [{ name: 'active', type: 'checkbox' }] },
777+
}],
778+
);
779+
expect(result).toEqual({ valid: true, errors: [] });
780+
});
781+
});
677782
});

0 commit comments

Comments
 (0)