Skip to content

Data and Type's Strategy

Michael Hladky edited this page Feb 26, 2024 · 3 revisions

Naming

To get good separation, and easier orientation in the code dependencies, naming of the core exposed types is essential. We try to distinguish between 2 main types exposed by packages to describe relations and responsibilities, "strict" and "un-strict"

The Flags type

The Flags naming is used to define values consumed over the process arguments. These values are wide and normally accept a number of overloads.

In many cases they represent a less strict type of the needed core arguments to execute a process or execute a method directly. e.g. npx code-pushup --verbose or for a specific command e.g. npx code-pushup collect --onlyPlugins=eslint,npm-audit

type SeperatedString = string; // e.g "a b c" or "a,b,c"

type MyFlags = {
  runs?: number;
  'upload.server'?: string;
  'only-plugins'?: string | SeperatedString | string[]
  onlyPlugins?: string | SeperatedString | string[]
};

The Raw type

The Raw type is similar to Flags but instead of describing process argument inputs it describes un-validated or parsed data.

The In type

The In type is similar to Flags but it describes user input over the terminal (stdin).

The Options type

The Options naming is used to define values consumed by the core logic. These types are most strict and can be trusted.
They could also be used by consumers directly without any interaction with the CLI. As this data is consumed over terminal input or the file system, this data needs to get validated before forwarded into the core logic.

type MyOptions = {
  runs: number;
  upload: {
    server: string
  };
  onlyPlugins: string[]
};

Intermediate types

Intermediate types are not part of the public API. In best this types are encapsulated into parser and validation logic. They should be specifically designed for certain situations or usecase and not much reused across the program.

Parsing and Validation

Parsing/validation should be used when data is coming from an unknown source:

  • parsing/validating ConfigRaw from rc.config.json to ConfigOptions
  • normalizing VerboseFlag to DebugOptions
  • validating prompt input (stdin) to OnlyPlugins

It is always done as early as possible and preferably on the most outer edge of the program. In our example we use zod as parser.

Packages and Types

The 3 main packages models, core, cli encapsulate a specific responsibility:

  1. model - maintain types and parser for the logic in core.
    Types used here are all kind of Option's as well as other more strict intermediate types.
  2. core - reuse final typing. All less strict types should be maintained in cli oth other consumer of core.
    Types exposed in core are most strict and always Option types.
  3. cli - reuses model and maintains all validations and parsing logic for less strict types. It executes the core logic. Types exposed here are less strict like Flags, Raw or In. Most of the less strict types are turned to strict types in this layer.
flowchart LR

Model
Core --> Model
Cli --> Model
Cli --> Core
Loading

Example

// === @code-pushup/model

type PersistOptions = {
  persist: {
    filename: string
  }
};
type PluginOptions = {
  plugins: string[]
};

// === @code-pushup/core

type GlobalOptions = {
  verbose: boolean
};

type CollectOnlyOptions = {
  onlyPlugins: string[]
};

type CollectOptions = GlobalOptions & PluginOptions & PersistOptions & CollectOnlyOptions;

type CollectLogic = (options: CollectOptions) => unknown;

// === @code-pushup/cli

// {persist: {filename: string}} => {persist?: {filename?: string}}
type DeepPartial<T> = { [Key in keyof T]?: Partial<T[Key]> };

//  {persist: {filename: string}} => {'persist.filename': string} 
type DotNotation<T> = { [Key in keyof T]?: Partial<T[Key]> };

type GlobalFlags = Partial<GlobalOptions>;

// Config
type ConfigFlags = { configPath?: string } & Partial<DotNotation<PersistOptions>>;

// --> Inside Middleware

type ConfigRaw = PluginOptions & DeepPartial<PersistOptions>;

// GlobalFlags to GlobalOptions
// ConfigFlags to DeepPartial<PersistOptions>
// GlobalFlags.configPath to ConfigRaw
// zod(DeepPartial<PersistOptions> & ConfigRaw & DEFAULTS): ConfigOptions

type ConfigMiddleware<T extends GlobalFlags & ConfigFlags & DEFAULTS> = (args: T) => GlobalOptions & ConfigOptions;
// ---|

// Collect
type SeperatedString = string; // e.g "a b c" or "a,b,c"
type CollectFlags = {
  onlyPlugins?: SeperatedString | string | string[]
};

// --> Inside Middleware

// CollectFlags to CollectOnlyOptions
// ConfigOptions to PluginOptions & PersistOptions
// zod(PluginOptions & PersistOptions & DEFAULTS_OR_ERROR): CollectOptions

type CollectMiddleware<T extends GlobalOptions & ConfigOptions & CollectFlags> = (args: T) => GlobalOptions & CollectOptions;
// ---|

// Call collectLogic