All files / session/profile resolveProfile.ts

97.82% Statements 45/46
93.33% Branches 28/30
100% Functions 10/10
97.43% Lines 38/39

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                  1x     112x 59x 59x 167x 167x 66x 66x 66x   2x   34x   27x                 96x 199x 199x 74x   125x                     56x 8x       20x 19x 19x 12x 12x   12x   8x 56x 28x 8x 8x 8x 16x   21x     8x 22x     8x 22x 7x    
import type {
  DeviceContext,
  ProfileOverrides,
  ResolvedProfile,
  SmartCubeProfile,
  SmartCubeProfilePatch,
} from './types.js';
import { mergeProfiles } from './mergeProfiles.js';
 
const keys = ['protocol', 'deviceName', 'deviceMAC', 'hardwareName', 'goCubeType'] as const;
 
function score(profile: SmartCubeProfile, context: DeviceContext): number {
  if (!profile.match) return profile.id === 'base' ? 0 : 1;
  let result = 0;
  for (const key of keys) {
    const expected = profile.match[key];
    if (!expected) continue;
    const actual = context[key];
    try {
      if (!actual || !new RegExp(`^(?:${expected})$`, 'i').test(actual)) return -1;
    } catch {
      return -1;
    }
    result += 10;
  }
  return result;
}
 
function recordLeafSources(
  value: object,
  source: string,
  sources: Record<string, string>,
  prefix = '',
): void {
  for (const [key, child] of Object.entries(value) as Array<[string, unknown]>) {
    const path = prefix ? `${prefix}.${key}` : key;
    if (child && typeof child === 'object' && !Array.isArray(child)) {
      recordLeafSources(child, source, sources, path);
    } else {
      sources[path] = source;
    }
  }
}
 
/** Select and resolve the most specific profile for a cube identity. */
export function resolveProfile(
  context: DeviceContext,
  profiles: readonly SmartCubeProfile[],
  overrides: ProfileOverrides = {},
): ResolvedProfile {
  const byId = new Map(profiles.map((profile) => [profile.id, profile]));
  const resolveChain = (
    profile: SmartCubeProfile,
    visiting = new Set<string>(),
  ): SmartCubeProfile[] => {
    if (visiting.has(profile.id)) throw new Error(`Profile inheritance cycle at '${profile.id}'`);
    const nextVisiting = new Set(visiting).add(profile.id);
    if (!profile.extends) return [profile];
    const parent = byId.get(profile.extends);
    Iif (!parent)
      throw new Error(`Profile '${profile.id}' extends missing profile '${profile.extends}'`);
    return [...resolveChain(parent, nextVisiting), profile];
  };
  const matches = profiles
    .filter((profile) => score(profile, context) >= 0)
    .sort((a, b) => score(a, context) - score(b, context));
  const selected = matches.at(-1) ?? byId.get('unknown')!;
  const chain = resolveChain(selected);
  const layers: Array<[string, SmartCubeProfile | SmartCubeProfilePatch]> = [
    ...chain.map((profile) => [profile.id, profile] as [string, SmartCubeProfile]),
    ...(['app', 'user', 'runtime'] as const).flatMap((layer) =>
      overrides[layer] ? [[layer, overrides[layer]] as [string, SmartCubeProfilePatch]] : [],
    ),
  ];
  const value = layers.reduce(
    (merged, [, layer]) => mergeProfiles(merged, layer),
    {} as SmartCubeProfile,
  );
  const sources: Record<string, string> = {};
  for (const [source, layer] of layers) recordLeafSources(layer, source, sources);
  return { id: selected.id, value, sources };
}