All files / server/services/course-content course-content-schema.ts

97.36% Statements 37/38
78.94% Branches 15/19
100% Functions 6/6
97.14% Lines 34/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141                12x 12x   12x       12x               12x       12x           12x           12x             12x         12x               12x                                       30x 30x 30x 30x 30x 30x               30x 30x 30x 29x 29x 29x   1x               30x 22x     22x 22x 21x     12x                     30x           30x       30x 30x    
import { z } from "zod";
import {
  examplesManifestSchema,
  manifestExampleSchema,
  validateManifestReferences,
  type ExamplesManifestCore,
} from "../examples/examples-schema";
 
const SAFE_ID = /^[a-z][a-z0-9-]{0,63}$/;
const v2ManifestExampleSchema = manifestExampleSchema.strict();
 
export const tutorDescriptorSchema = z.object({
  manifest: z.literal("tutor/manifest.yaml"),
}).strict();
 
const v2ManifestSchema = z.object({
  schemaVersion: z.literal(2),
  repository: z.string().max(256).optional(),
  ref: z.string().max(128).optional(),
  examples: z.array(v2ManifestExampleSchema).max(1000),
  tutor: tutorDescriptorSchema.optional(),
}).strict();
 
export const courseContentManifestSchema = z.union([examplesManifestSchema, v2ManifestSchema]);
 
export type TutorDescriptor = z.infer<typeof tutorDescriptorSchema>;
 
export const courseContentTopicEntrySchema = z.object({
  id: z.string().regex(SAFE_ID),
  path: z.string().regex(/^tutor\/topics\/[a-z][a-z0-9-]{0,63}\.yaml$/),
  sha256: z.string().regex(/^[a-f0-9]{64}$/i),
}).strict();
 
export const courseContentStrategyEntrySchema = z.object({
  id: z.string().regex(SAFE_ID),
  path: z.string().regex(/^tutor\/strategies\/[a-z][a-z0-9-]{0,63}\.yaml$/),
  sha256: z.string().regex(/^[a-f0-9]{64}$/i),
}).strict();
 
const tutorManifestV1Schema = z.object({
  schemaVersion: z.literal(1),
  defaultStrategy: z.string().regex(SAFE_ID).optional(),
  topics: z.array(courseContentTopicEntrySchema).max(64),
  strategies: z.array(courseContentStrategyEntrySchema).max(64),
}).strict();
 
const phaseStrategiesSchema = z.object({
  deepen: z.string().regex(SAFE_ID).optional(),
  expand: z.string().regex(SAFE_ID).optional(),
}).strict();
 
const tutorManifestV2Schema = z.object({
  schemaVersion: z.literal(2),
  defaultStrategy: z.string().regex(SAFE_ID).optional(),
  topics: z.array(courseContentTopicEntrySchema).max(64),
  strategies: z.array(courseContentStrategyEntrySchema).max(64),
  phaseStrategies: phaseStrategiesSchema.optional(),
}).strict();
 
export const courseContentTutorManifestSchema = z.union([tutorManifestV1Schema, tutorManifestV2Schema]);
 
export type CourseContentTutorManifest = z.infer<typeof courseContentTutorManifestSchema>;
 
type InvalidCapability = { readonly status: "invalid"; readonly reason: string };
 
export type CourseContentValidation = {
  readonly examples:
    | { readonly status: "valid"; readonly manifest: ExamplesManifestCore }
    | InvalidCapability;
  readonly tutor:
    | { readonly status: "absent" }
    | {
      readonly status: "valid";
      readonly descriptor: TutorDescriptor;
    }
    | InvalidCapability;
};
 
export function parseCourseContentManifest(input: unknown): CourseContentValidation {
  const raw = asRecord(input);
  const rawExamples = Array.isArray(raw?.examples) ? raw.examples : undefined;
  const examples = parseExamplesCapability(input, raw, rawExamples);
  const hasTutorRoot = raw !== undefined && hasOwn(raw, "tutor");
  const tutor = parseTutorCapability(raw, hasTutorRoot);
  return { examples, tutor };
}
 
function parseExamplesCapability(
  input: unknown,
  raw: Record<string, unknown> | undefined,
  rawExamples: unknown[] | undefined,
): CourseContentValidation["examples"] {
  const coreInput = raw === undefined ? input : withoutTutorData(raw, rawExamples);
  const coreParsed = courseContentCoreSchema.safeParse(coreInput);
  if (!coreParsed.success) return { status: "invalid", reason: "External Examples manifest is invalid" };
  try {
    validateManifestReferences(coreParsed.data);
    return { status: "valid", manifest: coreParsed.data };
  } catch {
    return { status: "invalid", reason: "External Examples snapshot is invalid" };
  }
}
 
function parseTutorCapability(
  raw: Record<string, unknown> | undefined,
  hasTutorCapability: boolean,
): CourseContentValidation["tutor"] {
  if (!hasTutorCapability) return { status: "absent" };
  Iif (raw?.schemaVersion !== 2) {
    return { status: "invalid", reason: "Tutor capability requires schema version 2" };
  }
  const descriptor = tutorDescriptorSchema.safeParse(raw.tutor);
  if (!descriptor.success) return { status: "invalid", reason: "Tutor descriptor is invalid" };
  return { status: "valid", descriptor: descriptor.data };
}
 
const courseContentCoreSchema = z.union([
  examplesManifestSchema,
  z.object({
    schemaVersion: z.literal(2),
    repository: z.string().max(256).optional(),
    ref: z.string().max(128).optional(),
    examples: z.array(v2ManifestExampleSchema).max(1000),
  }).strict(),
]);
 
function asRecord(value: unknown): Record<string, unknown> | undefined {
  return typeof value === "object" && value !== null && !Array.isArray(value)
    ? value as Record<string, unknown>
    : undefined;
}
 
function hasOwn(value: Record<string, unknown>, key: string): boolean {
  return Reflect.getOwnPropertyDescriptor(value, key) !== undefined;
}
 
function withoutTutorData(raw: Record<string, unknown>, _rawExamples: unknown[] | undefined): Record<string, unknown> {
  const { tutor: _tutor, ...core } = raw;
  return core;
}