Skip to content

Commit

Permalink
create more helpers and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
abrantesarthur committed Jan 11, 2025
1 parent e80c95e commit ec8e77c
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 37 deletions.
86 changes: 86 additions & 0 deletions src/helpers/createDefaultCodec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as t from 'io-ts';

/**
* Creates a default value for an io-ts codec.
*
* @param codec - the codec whose default we want to create
* @returns an object honoring the io-ts codec
*/
export const createDefaultCodec = <C extends t.Mixed>(
codec: C,
): t.TypeOf<C> => {
if (codec instanceof t.UnionType) {
// First, look for object types in the union
const objectType = codec.types.find(
(type: any) =>
type instanceof t.InterfaceType ||
type instanceof t.PartialType ||
type instanceof t.IntersectionType,
);
if (objectType) {
return createDefaultCodec(objectType);
}

// For unions, null has higher preference as default. Otherwise,first type's default
// If null is one of the union types, it should be the default
const hasNull = codec.types.some(
(type: any) => type instanceof t.NullType || type.name === 'null',
);
if (hasNull) {
return null as t.TypeOf<C>;
}

// If no null type found, default to first type
return createDefaultCodec(codec.types[0]);
}

if (codec instanceof t.InterfaceType || codec instanceof t.PartialType) {
const defaults: Record<string, any> = {};
Object.entries(codec.props).forEach(([key, type]) => {
defaults[key] = createDefaultCodec(type as any);
});
return defaults as t.TypeOf<C>;
}

if (codec instanceof t.IntersectionType) {
// Merge defaults of all types in the intersection
return codec.types.reduce(
(acc: t.TypeOf<C>, type: any) => ({
...acc,
...createDefaultCodec(type),
}),
{},
);
}

if (codec instanceof t.ArrayType) {
// Check if the array element type is an object type
const elementType = codec.type;
const isObjectType =
elementType instanceof t.InterfaceType ||
elementType instanceof t.PartialType ||
elementType instanceof t.IntersectionType;

return (
isObjectType ? [createDefaultCodec(elementType)] : []
) as t.TypeOf<C>;
}

// Handle primitive and common types
switch (codec.name) {
case 'string':
return '' as t.TypeOf<C>;
case 'number':
return null as t.TypeOf<C>;
case 'boolean':
return null as t.TypeOf<C>;
case 'null':
return null as t.TypeOf<C>;
case 'undefined':
return undefined as t.TypeOf<C>;
default:
return null as t.TypeOf<C>;
}
};
68 changes: 68 additions & 0 deletions src/helpers/enrichWithDefault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as t from 'io-ts';
import {
OneTrustAssessmentNestedQuestionCodec,
OneTrustAssessmentQuestionOptionCodec,
OneTrustAssessmentQuestionResponseCodec,
OneTrustAssessmentQuestionResponsesCodec,
OneTrustAssessmentQuestionRiskCodec,
OneTrustAssessmentQuestionRisksCodec,
OneTrustAssessmentSectionCodec,
OneTrustAssessmentSectionSubmittedByCodec,
OneTrustPrimaryEntityDetailsCodec,
} from '../oneTrust/codecs';
import { createDefaultCodec } from './createDefaultCodec';

// TODO: test the shit out of this
const enrichQuestionWithDefault = ({
options,
...rest
}: OneTrustAssessmentNestedQuestionCodec): OneTrustAssessmentNestedQuestionCodec => ({
options:
options === null || options.length === 0
? createDefaultCodec(t.array(OneTrustAssessmentQuestionOptionCodec))
: options,
...rest,
});

// TODO: test the shit out of this
const enrichQuestionResponsesWithDefault = (
questionResponses: OneTrustAssessmentQuestionResponsesCodec,
): OneTrustAssessmentQuestionResponsesCodec =>
questionResponses.length === 0
? createDefaultCodec(t.array(OneTrustAssessmentQuestionResponseCodec))
: questionResponses;

// TODO: test the shit out of this
const enrichRisksWithDefault = (
risks: OneTrustAssessmentQuestionRisksCodec,
): OneTrustAssessmentQuestionRisksCodec =>
risks === null || risks.length === 0
? createDefaultCodec(t.array(OneTrustAssessmentQuestionRiskCodec))
: risks;

// TODO: test the shit out of this
export const enrichSectionsWithDefault = (
sections: OneTrustAssessmentSectionCodec[],
): OneTrustAssessmentSectionCodec[] =>
sections.map((s) => ({
...s,
questions: s.questions.map((q) => ({
...q,
question: enrichQuestionWithDefault(q.question),
questionResponses: enrichQuestionResponsesWithDefault(
q.questionResponses,
),
risks: enrichRisksWithDefault(q.risks),
})),
submittedBy:
s.submittedBy === null
? createDefaultCodec(OneTrustAssessmentSectionSubmittedByCodec)
: s.submittedBy,
}));

export const enrichPrimaryEntityDetailsWithDefault = (
primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec,
): OneTrustPrimaryEntityDetailsCodec =>
primaryEntityDetails.length === 0
? createDefaultCodec(OneTrustPrimaryEntityDetailsCodec)
: primaryEntityDetails;
2 changes: 2 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from './buildEnabledRouteType';
export * from './inquirer';
export * from './parseVariablesFromString';
export * from './extractProperties';
export * from './createDefaultCodec';
export * from './enrichWithDefault';
100 changes: 100 additions & 0 deletions src/helpers/tests/createDefaultCodec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as t from 'io-ts';
import chai, { expect } from 'chai';
import deepEqualInAnyOrder from 'deep-equal-in-any-order';

import { createDefaultCodec } from '../createDefaultCodec';

chai.use(deepEqualInAnyOrder);

describe('buildDefaultCodec', () => {
it('should correctly build a default codec for null', () => {
const result = createDefaultCodec(t.null);
expect(result).to.equal(null);
});

it('should correctly build a default codec for number', () => {
const result = createDefaultCodec(t.number);
expect(result).to.equal(null);
});

it('should correctly build a default codec for boolean', () => {
const result = createDefaultCodec(t.boolean);
expect(result).to.equal(null);
});

it('should correctly build a default codec for undefined', () => {
const result = createDefaultCodec(t.undefined);
expect(result).to.equal(undefined);
});

it('should correctly build a default codec for string', () => {
const result = createDefaultCodec(t.string);
expect(result).to.equal('');
});

it('should correctly build a default codec for a union with null', () => {
const result = createDefaultCodec(t.union([t.string, t.null]));
// should default to null if the union contains null
expect(result).to.equal(null);
});

it('should correctly build a default codec for a union with type', () => {
const result = createDefaultCodec(
t.union([t.string, t.null, t.type({ name: t.string })]),
);
// should default to the type if the union contains a type
expect(result).to.deep.equal({ name: '' });
});

it('should correctly build a default codec for a union without null', () => {
const result = createDefaultCodec(t.union([t.string, t.number]));
// should default to the first value if the union does not contains null
expect(result).to.equal('');
});

it('should correctly build a default codec for an array of object types', () => {
const result = createDefaultCodec(
t.array(t.type({ name: t.string, age: t.number })),
);
// should default to the first value if the union does not contains null
expect(result).to.deep.equalInAnyOrder([{ name: '', age: null }]);
});

it('should correctly build a default codec for an array of object partials', () => {
const result = createDefaultCodec(
t.array(t.partial({ name: t.string, age: t.number })),
);
// should default to the first value if the union does not contains null
expect(result).to.deep.equalInAnyOrder([{ name: '', age: null }]);
});

it('should correctly build a default codec for an array of object intersections', () => {
const result = createDefaultCodec(
t.array(
t.intersection([
t.partial({ name: t.string, age: t.number }),
t.type({ city: t.string }),
]),
),
);
// should default to the first value if the union does not contains null
expect(result).to.deep.equalInAnyOrder([{ name: '', age: null, city: '' }]);
});

it('should correctly build a default codec for an array of strings', () => {
const result = createDefaultCodec(t.array(t.string));
// should default to the first value if the union does not contains null
expect(result).to.deep.equal([]);
});

it('should correctly build a default codec for an intersection', () => {
const result = createDefaultCodec(
t.intersection([
t.type({ id: t.string, name: t.string }),
t.partial({ age: t.number }),
]),
);
// should default to the first value if the union does not contains null
expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null });
});
});
72 changes: 51 additions & 21 deletions src/oneTrust/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ export type OneTrustAssessmentQuestionRiskCodec = t.TypeOf<
typeof OneTrustAssessmentQuestionRiskCodec
>;

export const OneTrustAssessmentQuestionRisksCodec = t.union([
t.array(OneTrustAssessmentQuestionRiskCodec),
t.null,
]);
/** Type override */
export type OneTrustAssessmentQuestionRisksCodec = t.TypeOf<
typeof OneTrustAssessmentQuestionRisksCodec
>;

export const OneTrustAssessmentQuestionResponseCodec = t.type({
/** The responses */
responses: t.array(
Expand Down Expand Up @@ -227,6 +236,14 @@ export type OneTrustAssessmentQuestionResponseCodec = t.TypeOf<
typeof OneTrustAssessmentQuestionResponseCodec
>;

export const OneTrustAssessmentQuestionResponsesCodec = t.array(
OneTrustAssessmentQuestionResponseCodec,
);
/** Type override */
export type OneTrustAssessmentQuestionResponsesCodec = t.TypeOf<
typeof OneTrustAssessmentQuestionResponsesCodec
>;

export const OneTrustAssessmentNestedQuestionCodec = t.type({
/** ID of the question. */
id: t.string,
Expand Down Expand Up @@ -449,6 +466,21 @@ export type OneTrustAssessmentSectionFlatHeaderCodec = t.TypeOf<
typeof OneTrustAssessmentSectionFlatHeaderCodec
>;

export const OneTrustAssessmentSectionSubmittedByCodec = t.union([
t.type({
/** The ID of the user who submitted the section */
id: t.string,
/** THe name or email of the user who submitted the section */
name: t.string,
}),
t.null,
]);

/** Type override */
export type OneTrustAssessmentSectionSubmittedByCodec = t.TypeOf<
typeof OneTrustAssessmentSectionSubmittedByCodec
>;

export const OneTrustAssessmentSectionCodec = t.type({
/** The Assessment section header */
header: OneTrustAssessmentSectionHeaderCodec,
Expand All @@ -457,15 +489,7 @@ export const OneTrustAssessmentSectionCodec = t.type({
/** Indicates whether navigation rules are enabled for the question. */
hasNavigationRules: t.boolean,
/** Who submitted the section */
submittedBy: t.union([
t.type({
/** The ID of the user who submitted the section */
id: t.string,
/** THe name or email of the user who submitted the section */
name: t.string,
}),
t.null,
]),
submittedBy: OneTrustAssessmentSectionSubmittedByCodec,
/** Date of the submission */
submittedDt: t.union([t.string, t.null]),
/** Name of the section. */
Expand Down Expand Up @@ -564,6 +588,23 @@ export type OneTrustAssessmentStatusCodec = t.TypeOf<
typeof OneTrustAssessmentStatusCodec
>;

export const OneTrustPrimaryEntityDetailsCodec = t.array(
t.type({
/** Unique ID for the primary record. */
id: t.string,
/** Name of the primary record. */
name: t.string,
/** The number associated with the primary record. */
number: t.number,
/** Name and number of the primary record. */
displayName: t.string,
}),
);
/** Type override */
export type OneTrustPrimaryEntityDetailsCodec = t.TypeOf<
typeof OneTrustPrimaryEntityDetailsCodec
>;

// ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget
export const OneTrustGetAssessmentResponseCodec = t.type({
/** List of users assigned as approvers of the assessment. */
Expand Down Expand Up @@ -611,18 +652,7 @@ export const OneTrustGetAssessmentResponseCodec = t.type({
name: t.string,
}),
/** The primary record */
primaryEntityDetails: t.array(
t.type({
/** Unique ID for the primary record. */
id: t.string,
/** Name of the primary record. */
name: t.string,
/** The number associated with the primary record. */
number: t.number,
/** Name and number of the primary record. */
displayName: t.string,
}),
),
primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec,
/** Type of inventory record designated as the primary record. */
primaryRecordType: t.union([
t.literal('ASSETS'),
Expand Down
Loading

0 comments on commit ec8e77c

Please sign in to comment.