@cleverbrush/mapper 0.0.0-beta-20260410073748

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # @cleverbrush/mapper
2
+
3
+ [![CI](https://github.com/cleverbrush/framework/actions/workflows/ci.yml/badge.svg)](https://github.com/cleverbrush/framework/actions/workflows/ci.yml)
4
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](../../LICENSE)
5
+ <!-- coverage-badge-start -->
6
+ ![Coverage](https://img.shields.io/badge/coverage-96.8%25-brightgreen)
7
+ <!-- coverage-badge-end -->
8
+
9
+ A type-safe, declarative object mapper for converting objects between different `@cleverbrush/schema` representations. Uses PropertyDescriptors as pointers to properties (similar to expressions in C# .NET) and enforces **compile-time completeness** — TypeScript will produce an error if any target property is not mapped, auto-mapped, or explicitly ignored.
10
+
11
+ ## Why @cleverbrush/mapper?
12
+
13
+ **The problem:** Converting between different object shapes — API responses to domain models, domain models to DTOs, database rows to view models — is tedious and error-prone. You write manual mapping functions full of `destination.x = source.y` assignments. Add a new property to a schema and nothing tells you the mapper is incomplete. The bug shows up at runtime, not at compile time.
14
+
15
+ **The solution:** `@cleverbrush/mapper` uses **PropertyDescriptor-based selectors** (similar to C# expression trees) for type-safe property mapping. The TypeScript compiler enforces that **every target property is mapped** — unmapped properties cause a compile-time error. You literally cannot forget a field.
16
+
17
+ **What makes it different:**
18
+
19
+ - **Compile-time completeness** — unmapped properties are a TypeScript error, not a runtime surprise
20
+ - **Type-safe selectors** — `.for((t) => t.name).from((s) => s.name)` — fully checked at compile time, not string-based
21
+ - **Auto-mapping** — properties with the same name and compatible type are mapped automatically; you only configure what differs
22
+ - **Immutable registry** — `configure()` returns a new registry; safe to share and extend
23
+ - **No decorators or classes** — works with plain objects and schemas
24
+
25
+ | Feature | @cleverbrush/mapper | AutoMapper-ts | class-transformer | morphism |
26
+ | --- | --- | --- | --- | --- |
27
+ | Compile-time completeness | ✓ | ✗ | ✗ | ✗ |
28
+ | Type-safe selectors | ✓ | ✗ | ✗ | ✗ |
29
+ | No decorators required | ✓ | ✗ | ✗ | ✓ |
30
+ | Works without classes | ✓ | ✗ | ✗ | ✓ |
31
+ | Auto-mapping | ✓ | ✓ | ✗ | ✗ |
32
+ | Immutable registry | ✓ | ✗ | ✗ | ✗ |
33
+ | Nested schema support | ✓ | ~ | ~ | ✗ |
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install @cleverbrush/mapper
39
+ ```
40
+
41
+ **Peer dependency:** `@cleverbrush/schema`
42
+
43
+ ## Quick Start
44
+
45
+ ```typescript
46
+ import { object, string, number } from '@cleverbrush/schema';
47
+ import { mapper } from '@cleverbrush/mapper';
48
+
49
+ // Define source and target schemas
50
+ const ApiUser = object({
51
+ first_name: string(),
52
+ last_name: string(),
53
+ birth_year: number()
54
+ });
55
+
56
+ const DomainUser = object({
57
+ fullName: string(),
58
+ age: number()
59
+ });
60
+
61
+ // Configure the mapping — returns a new (immutable) registry
62
+ const registry = mapper().configure(
63
+ ApiUser,
64
+ DomainUser,
65
+ (m) =>
66
+ m
67
+ .for((t) => t.fullName)
68
+ .compute((src) => src.first_name + ' ' + src.last_name)
69
+ .for((t) => t.age)
70
+ .compute((src) => new Date().getFullYear() - src.birth_year)
71
+ );
72
+
73
+ // Get the mapper function and use it
74
+ const mapFn = registry.getMapper(ApiUser, DomainUser);
75
+
76
+ const dto = await mapFn({
77
+ first_name: 'Jane',
78
+ last_name: 'Doe',
79
+ birth_year: 1995
80
+ });
81
+ // { fullName: 'Jane Doe', age: <current year - 1995> }
82
+ ```
83
+
84
+ ## How It Works — Step by Step
85
+
86
+ 1. **Define schemas** — use `@cleverbrush/schema` to define source and target shapes
87
+ 2. **Configure mappings** — use `.for()` to select a target property, then `.from()`, `.compute()`, or `.ignore()` to define how it's populated
88
+ 3. **Auto-mapping fills the gaps** — properties with the same name and compatible type are mapped automatically
89
+ 4. **Get a mapper function** — `registry.getMapper(from, to)` returns an async function that transforms objects
90
+ 5. **TypeScript enforces completeness** — if any target property is unmapped, you get a compile-time error
91
+
92
+ ## Compile-Time Safety
93
+
94
+ The mapper enforces multiple layers of compile-time safety:
95
+
96
+ ### Unmapped properties
97
+
98
+ Every target property must be either mapped, auto-mapped, or explicitly ignored. If you forget to map a property, TypeScript will produce a compile-time error on the `configure` callback return:
99
+
100
+ ```typescript
101
+ mapper().configure(
102
+ UserSchema,
103
+ UserDtoSchema,
104
+ (m) =>
105
+ m
106
+ .for((t) => t.name)
107
+ .from((f) => f.name)
108
+ .for((t) => t.cityName)
109
+ .from((f) => f.address.city)
110
+ // TS Error: Type 'Mapper<..., "fullAddress", ...>' is not assignable to
111
+ // type 'Mapper<..., never, ...>'.
112
+ // Types of property '[SYMBOL_UNMAPPED]' are incompatible.
113
+ // Type '"fullAddress"' is not assignable to type 'never'.
114
+ );
115
+ ```
116
+
117
+ The error message shows the names of the unmapped properties directly in the type mismatch.
118
+
119
+ ### Type-incompatible `from`
120
+
121
+ `from` only shows source properties whose `InferType` is assignable to the target property's type. If you select an incompatible property (e.g., mapping a `number` to a `string`), TypeScript produces a compile-time error:
122
+
123
+ ```typescript
124
+ // Trying to map a string target from a number source
125
+ m.for((t) => t.cityName).from((f) => f.houseNr)
126
+ // TS Error: source property type is not assignable to target property type
127
+ // Use .compute() instead to transform the value
128
+ ```
129
+
130
+ ### Unregistered nested mappings
131
+
132
+ When `from` maps between two `ObjectSchemaBuilder` properties, a mapping for that schema pair must be registered in the registry first. Otherwise, TypeScript produces a compile-time error:
133
+
134
+ ```typescript
135
+ const PersonSchema = object({ name: string(), address: AddressSchema });
136
+ const PersonDtoSchema = object({ name: string(), address: AddressDtoSchema });
137
+
138
+ // Error — AddressSchema→AddressDtoSchema is not registered
139
+ mapper().configure(
140
+ PersonSchema,
141
+ PersonDtoSchema,
142
+ (m) =>
143
+ m
144
+ .for((t) => t.name)
145
+ .from((f) => f.name)
146
+ .for((t) => t.address)
147
+ .from((f) => f.address) // TS Error: Register a mapping for the
148
+ // source→target schema pair first
149
+ );
150
+ ```
151
+
152
+ ## Auto-Mapping
153
+
154
+ Properties that can be automatically determined don't need explicit mapping configuration. Auto-mapping activates in two scenarios:
155
+
156
+ ### Same-name, same-type primitives
157
+
158
+ When the source and target schemas have a property with the **same name** and **compatible `InferType`**, it is auto-mapped automatically:
159
+
160
+ ```typescript
161
+ const Source = object({
162
+ id: string(),
163
+ name: string(),
164
+ email: string(),
165
+ age: number()
166
+ });
167
+
168
+ const Target = object({
169
+ id: string(), // same name + type → auto-mapped
170
+ name: string(), // same name + type → auto-mapped
171
+ email: string(), // same name + type → auto-mapped
172
+ ageGroup: string() // different name → must be configured
173
+ });
174
+
175
+ const registry = mapper().configure(Source, Target, (m) =>
176
+ m
177
+ .for((t) => t.ageGroup)
178
+ .compute((src) => src.age < 18 ? 'minor' : 'adult')
179
+ // id, name, email are auto-mapped — no configuration needed!
180
+ );
181
+ ```
182
+
183
+ ### Nested ObjectSchemaBuilder properties
184
+
185
+ When both the source and target have a same-name property that is an `ObjectSchemaBuilder`, and a mapping for that schema pair has been previously registered, the nested property is auto-mapped using the registered mapper:
186
+
187
+ ```typescript
188
+ const AddressSchema = object({ city: string(), houseNr: number() });
189
+ const AddressDtoSchema = object({ city: string() });
190
+
191
+ const PersonSchema = object({ name: string(), address: AddressSchema });
192
+ const PersonDtoSchema = object({ name: string(), address: AddressDtoSchema });
193
+
194
+ const registry = mapper()
195
+ // Register Address mapping first
196
+ .configure(AddressSchema, AddressDtoSchema, (m) =>
197
+ m.for((t) => t.city).from((f) => f.city)
198
+ )
199
+ // address is auto-mapped using the registered AddressSchema→AddressDtoSchema mapper
200
+ .configure(PersonSchema, PersonDtoSchema, (m) =>
201
+ m.for((t) => t.name).from((f) => f.name)
202
+ );
203
+
204
+ const mapFn = registry.getMapper(PersonSchema, PersonDtoSchema);
205
+ const result = await mapFn({
206
+ name: 'Alice',
207
+ address: { city: 'Berlin', houseNr: 10 }
208
+ });
209
+ // result: { name: 'Alice', address: { city: 'Berlin' } }
210
+ ```
211
+
212
+ **Ordering matters:** nested mappings must be registered before the parent mapping. Explicit mappings via `compute` or `ignore` take priority over auto-mapping.
213
+
214
+ ## Mapping Strategies
215
+
216
+ | Strategy | Usage | Purpose |
217
+ | --- | --- | --- |
218
+ | `.from(selector)` | `.for(t => t.x).from(s => s.y)` | Copy from a source property (supports nested paths) |
219
+ | `.compute(fn)` | `.for(t => t.x).compute(s => s.a + s.b)` | Compute from a sync or async function |
220
+ | `.ignore()` | `.for(t => t.x).ignore()` | Exclude a target property |
221
+ | _(auto-mapped)_ | _(no configuration needed)_ | Same-name, compatible-type primitives or registered nested schemas |
222
+
223
+ Every non-auto-mappable target property must be either mapped or explicitly ignored. Unmapped properties cause:
224
+
225
+ - A **compile-time TypeScript type error** — a type-assignability mismatch that includes the unmapped property names in the type parameters
226
+ - A **runtime `MapperConfigurationError`** if type checks are bypassed
227
+
228
+ ## API
229
+
230
+ ### `mapper()`
231
+
232
+ A convenience factory function that creates a new `MappingRegistry`:
233
+
234
+ ```typescript
235
+ const registry = mapper()
236
+ .configure(A, B, (m) => ...)
237
+ .configure(C, D, (m) => ...);
238
+ ```
239
+
240
+ ### `registry.configure(fromSchema, toSchema, fn)`
241
+
242
+ Defines a mapping between two schemas and returns a new immutable registry containing the mapping. The callback `fn` receives a fresh `Mapper` and must return it after configuring all non-auto-mappable property mappings.
243
+
244
+ Throws if schemas are invalid, the mapping is a duplicate, or if unmapped properties remain that cannot be auto-mapped.
245
+
246
+ ### `registry.getMapper(fromSchema, toSchema)`
247
+
248
+ Retrieves a previously registered mapper function. Throws if no mapper has been registered for the given schema pair.
249
+
250
+ ```typescript
251
+ const mapFn = registry.getMapper(ApiUser, DomainUser);
252
+ const result = await mapFn(sourceObject);
253
+ ```
254
+
255
+ ### `Mapper`
256
+
257
+ A fluent builder for configuring how each target property is populated:
258
+
259
+ - **`.for(selector)`** — selects a target property to configure
260
+ - **`.from(selector)`** — maps from a source property (types must be compatible)
261
+ - **`.compute(fn)`** — computes the value from the entire source object (sync or async)
262
+ - **`.ignore()`** — explicitly excludes the property
263
+ - **`.getMapper()`** — returns the mapping function (only available when all properties are mapped)
264
+
265
+ ```typescript
266
+ const mapper = new Mapper(SourceSchema, TargetSchema);
267
+ const mapFn = mapper
268
+ .for((t) => t.fullName)
269
+ .compute((src) => `${src.firstName} ${src.lastName}`)
270
+ .for((t) => t.age)
271
+ .from((src) => src.years)
272
+ .getMapper();
273
+
274
+ const result = await mapFn(sourceObject);
275
+ ```
276
+
277
+ ## Real-World Example
278
+
279
+ A complete example mapping API responses through multiple layers:
280
+
281
+ ```typescript
282
+ import { object, string, number } from '@cleverbrush/schema';
283
+ import { mapper } from '@cleverbrush/mapper';
284
+
285
+ // API response shape
286
+ const ApiOrderResponse = object({
287
+ order_id: string(),
288
+ customer_name: string(),
289
+ total_cents: number(),
290
+ status_code: number()
291
+ });
292
+
293
+ // Domain model
294
+ const Order = object({
295
+ id: string(),
296
+ customer: string(),
297
+ totalPrice: string(),
298
+ status: string()
299
+ });
300
+
301
+ const registry = mapper().configure(
302
+ ApiOrderResponse,
303
+ Order,
304
+ (m) =>
305
+ m
306
+ .for((t) => t.id)
307
+ .from((s) => s.order_id)
308
+ .for((t) => t.customer)
309
+ .from((s) => s.customer_name)
310
+ .for((t) => t.totalPrice)
311
+ .compute((s) => `$${(s.total_cents / 100).toFixed(2)}`)
312
+ .for((t) => t.status)
313
+ .compute((s) => {
314
+ const statuses: Record<number, string> = {
315
+ 0: 'pending', 1: 'confirmed', 2: 'shipped', 3: 'delivered'
316
+ };
317
+ return statuses[s.status_code] ?? 'unknown';
318
+ })
319
+ );
320
+
321
+ const mapOrder = registry.getMapper(ApiOrderResponse, Order);
322
+ const order = await mapOrder({
323
+ order_id: 'ORD-123',
324
+ customer_name: 'Alice Smith',
325
+ total_cents: 4999,
326
+ status_code: 2
327
+ });
328
+ // { id: 'ORD-123', customer: 'Alice Smith', totalPrice: '$49.99', status: 'shipped' }
329
+ ```
330
+
331
+ ## Code Quality
332
+
333
+ - **Linting:** [Biome](https://biomejs.dev/) — enforced on every PR via CI
334
+ - **Type checking:** TypeScript strict mode — all type selectors and mapping configurations are validated at compile time
335
+ - **Unit tests:** [Vitest](https://vitest.dev/) — runtime tests + type-level tests (`expectTypeOf`) covering auto-mapping, computed fields, nested schemas, and compile-time completeness errors
336
+ - **CI:** Every pull request must pass lint + build + test before merge — see [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)
337
+
338
+ ## License
339
+
340
+ BSD-3-Clause
@@ -0,0 +1,258 @@
1
+ import { ArraySchemaBuilder, type InferType, ObjectSchemaBuilder, type PropertyDescriptor, type PropertyDescriptorTree, type SchemaBuilder, SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR } from '@cleverbrush/schema';
2
+ /**
3
+ * Internal symbol used to brand target property descriptors with their key.
4
+ * Using a symbol instead of a string property keeps it out of IntelliSense.
5
+ */
6
+ declare const SYMBOL_TARGET_PROPERTY_KEY: unique symbol;
7
+ /**
8
+ * Internal symbol used as a phantom property key on Mapper to track
9
+ * unmapped target properties. Using a symbol keeps it out of IntelliSense.
10
+ */
11
+ declare const SYMBOL_UNMAPPED: unique symbol;
12
+ /**
13
+ * Extracts the properties record from an ObjectSchemaBuilder.
14
+ */
15
+ type ExtractSchemaProperties<T> = T extends ObjectSchemaBuilder<infer TProperties, any, any> ? TProperties : never;
16
+ /**
17
+ * Gets all top-level property key names of an ObjectSchemaBuilder.
18
+ */
19
+ type SchemaKeys<T extends ObjectSchemaBuilder<any, any, any>> = keyof ExtractSchemaProperties<T> & string;
20
+ /**
21
+ * Branded phantom type that tags a property descriptor with its key name.
22
+ * This allows TypeScript to infer which property was selected in the
23
+ * `for` callback, enabling compile-time tracking of mapped vs unmapped
24
+ * properties.
25
+ */
26
+ type TargetPropertyKey<K extends string> = {
27
+ readonly [SYMBOL_TARGET_PROPERTY_KEY]: K;
28
+ };
29
+ /**
30
+ * Creates a tree of selectable target properties, filtered to only show
31
+ * properties whose keys are in `TAllowedKeys`. Each property is branded
32
+ * with `TargetPropertyKey<K>` so the key can be inferred from the return
33
+ * type of the selector callback.
34
+ */
35
+ type TargetPropertyTree<TSchema extends ObjectSchemaBuilder<any, any, any>, TAllowedKeys extends string> = {
36
+ [K in SchemaKeys<TSchema> & TAllowedKeys]: TargetPropertyKey<K> & PropertyDescriptor<TSchema, ExtractSchemaProperties<TSchema>[K], any>;
37
+ };
38
+ /**
39
+ * Infers the TypeScript type of a specific property in an ObjectSchemaBuilder
40
+ * by its key name.
41
+ */
42
+ type SchemaPropertyInferredType<TSchema extends ObjectSchemaBuilder<any, any, any>, K extends string> = K extends keyof ExtractSchemaProperties<TSchema> ? InferType<ExtractSchemaProperties<TSchema>[K]> : never;
43
+ /**
44
+ * Extracts the schema (SchemaBuilder) of a specific property in an
45
+ * ObjectSchemaBuilder by its key name.
46
+ */
47
+ type TargetPropertySchema<TSchema extends ObjectSchemaBuilder<any, any, any>, K extends string> = K extends keyof ExtractSchemaProperties<TSchema> ? ExtractSchemaProperties<TSchema>[K] : never;
48
+ /**
49
+ * Extracts the element schema from an ArraySchemaBuilder.
50
+ */
51
+ type ExtractArrayElementSchema<T> = T extends ArraySchemaBuilder<infer TElementSchema, any, any> ? TElementSchema : never;
52
+ /**
53
+ * Extracts the property schema from a PropertyDescriptor's `getSchema()`
54
+ * return type. Used by {@link IsFromCompatible} to recover the source
55
+ * property's schema from the inferred `TReturn` of the `from()` selector.
56
+ */
57
+ type ExtractPropertySchema<T> = T extends {
58
+ [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {
59
+ getSchema(): infer TSchema;
60
+ };
61
+ } ? TSchema : never;
62
+ /**
63
+ * Determines whether a property needs an explicit mapping configuration.
64
+ *
65
+ * - Both are ObjectSchemaBuilder with registered mapping → `false` (auto-mappable)
66
+ * - Both are ObjectSchemaBuilder without registered mapping → `true`
67
+ * - Either is ObjectSchemaBuilder but the other is not → `true`
68
+ * - Both are ArraySchemaBuilder with element schemas that have registered mapping → `false`
69
+ * - Both are ArraySchemaBuilder with same InferType → `false`
70
+ * - Array vs non-array → `true`
71
+ * - Neither is ObjectSchemaBuilder + InferType<source> extends InferType<target>
72
+ * → `false` (same-name, compatible primitive types → auto-mappable)
73
+ * - Neither is ObjectSchemaBuilder + incompatible InferType → `true`
74
+ */
75
+ type NeedsMapping<TSourcePropSchema, TTargetPropSchema, TRegistered> = TSourcePropSchema extends ArraySchemaBuilder<any, any, any> ? TTargetPropSchema extends ArraySchemaBuilder<any, any, any> ? NeedsMapping<ExtractArrayElementSchema<TSourcePropSchema>, ExtractArrayElementSchema<TTargetPropSchema>, TRegistered> : true : TTargetPropSchema extends ArraySchemaBuilder<any, any, any> ? true : TSourcePropSchema extends ObjectSchemaBuilder<any, any, any> ? TTargetPropSchema extends ObjectSchemaBuilder<any, any, any> ? [TSourcePropSchema, TTargetPropSchema] extends TRegistered ? false : InferType<TSourcePropSchema> extends InferType<TTargetPropSchema> ? InferType<TTargetPropSchema> extends InferType<TSourcePropSchema> ? false : true : true : true : TTargetPropSchema extends ObjectSchemaBuilder<any, any, any> ? true : InferType<TSourcePropSchema> extends InferType<TTargetPropSchema> ? false : true;
76
+ /**
77
+ * From all keys of the target schema, filter down to only those that
78
+ * require explicit mapping (i.e. `NeedsMapping` is `true`).
79
+ * Keys where `NeedsMapping` is `false` can be auto-mapped via the registry.
80
+ */
81
+ type KeysNeedingMapping<TFromSchema extends ObjectSchemaBuilder<any, any, any>, TToSchema extends ObjectSchemaBuilder<any, any, any>, TRegistered> = {
82
+ [K in SchemaKeys<TToSchema>]: K extends SchemaKeys<TFromSchema> ? NeedsMapping<ExtractSchemaProperties<TFromSchema>[K], ExtractSchemaProperties<TToSchema>[K], TRegistered> extends true ? K : never : K;
83
+ }[SchemaKeys<TToSchema>];
84
+ /**
85
+ * Checks whether two property schemas are compatible, considering
86
+ * registered mappings. Mirrors {@link NeedsMapping}'s structure:
87
+ * receives schemas as direct type parameters, uses
88
+ * {@link ExtractArrayElementSchema} for arrays, and checks registration
89
+ * via tuple-extends-union (not inline `infer`).
90
+ */
91
+ type CheckSchemaCompatible<TSourceSchema, TTargetSchema, TRegistered> = [
92
+ InferType<TSourceSchema>
93
+ ] extends [InferType<TTargetSchema>] ? true : TSourceSchema extends ArraySchemaBuilder<any, any, any> ? TTargetSchema extends ArraySchemaBuilder<any, any, any> ? CheckSchemaCompatible<ExtractArrayElementSchema<TSourceSchema>, ExtractArrayElementSchema<TTargetSchema>, TRegistered> : false : TSourceSchema extends ObjectSchemaBuilder<any, any, any> ? TTargetSchema extends ObjectSchemaBuilder<any, any, any> ? [TSourceSchema, TTargetSchema] extends TRegistered ? true : false : false : false;
94
+ /**
95
+ * Checks whether the source property selected by `from()` is compatible
96
+ * with the target property, evaluated *after* `TReturn` has been inferred.
97
+ *
98
+ * This decouples inference from validation: the `TReturn` constraint uses
99
+ * `any` for setValue/getValue value types so TypeScript always infers
100
+ * successfully, and this type performs the actual compatibility check
101
+ * in the `_args` conditional spread.
102
+ */
103
+ type IsFromCompatible<TReturn, TToSchema extends ObjectSchemaBuilder<any, any, any>, TKey extends string, TRegistered> = [ExtractPropertySchema<TReturn>] extends [never] ? false : CheckSchemaCompatible<ExtractPropertySchema<TReturn>, TargetPropertySchema<TToSchema, TKey>, TRegistered>;
104
+ export type SchemaToSchemaMapperResult<TFromSchema extends ObjectSchemaBuilder<any, any, any>, TToSchema extends ObjectSchemaBuilder<any, any, any>> = (from: InferType<TFromSchema>) => Promise<InferType<TToSchema>>;
105
+ export declare class MapperConfigurationError extends Error {
106
+ constructor(messageOrUnmappedProperties: string | string[]);
107
+ }
108
+ /**
109
+ * Intermediate builder returned by `for()`. Provides three strategies
110
+ * to configure how the selected target property is populated:
111
+ * - `from()` — copy from a source property
112
+ * - `compute()` — compute from the entire source object
113
+ * - `ignore()` — explicitly skip the property
114
+ */
115
+ export declare class PropertyMappingBuilder<TFromSchema extends ObjectSchemaBuilder<any, any, any>, TToSchema extends ObjectSchemaBuilder<any, any, any>, TKey extends string, TUnmapped extends string, TRegistered = never> {
116
+ private readonly _mapper;
117
+ private readonly _targetKey;
118
+ /** @internal */
119
+ constructor(mapper: Mapper<TFromSchema, TToSchema, any, TRegistered>, targetKey: TKey);
120
+ /**
121
+ * Maps the target property from a source property. The selector
122
+ * receives the source schema's PropertyDescriptorTree and supports
123
+ * nested paths (e.g. `(s) => s.address.city`).
124
+ *
125
+ * Type compatibility is enforced: only source properties whose
126
+ * inferred type is assignable to the target property type will
127
+ * appear in the selector callback.
128
+ *
129
+ * Under `strictFunctionTypes`, the `setValue` and `getValue` constraints
130
+ * provide bidirectional type checking:
131
+ * - `setValue` contravariance rejects source schemas with extra properties
132
+ * - `getValue` covariance rejects source schemas with missing properties
133
+ * - registered mappings widen the constraint via intersection
134
+ */
135
+ from<TReturn extends {
136
+ [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {
137
+ getSchema(): SchemaBuilder<any, any, any>;
138
+ setValue: (obj: any, value: any) => any;
139
+ getValue: (obj: any) => {
140
+ value?: any;
141
+ success: boolean;
142
+ };
143
+ };
144
+ }>(selector: (tree: PropertyDescriptorTree<TFromSchema, TFromSchema, SchemaPropertyInferredType<TToSchema, TKey>>) => TReturn, ..._args: [TReturn] extends [never] ? [
145
+ error: `Property '${TKey}': source property type is not assignable to the target property type. Use compute() instead.`
146
+ ] : IsFromCompatible<TReturn, TToSchema, TKey, TRegistered> extends true ? [] : [
147
+ error: `Property '${TKey}': source property type is not assignable to the target property type. Use compute() instead.`
148
+ ]): Mapper<TFromSchema, TToSchema, Exclude<TUnmapped, TKey>, TRegistered>;
149
+ /**
150
+ * Computes the target property value from the entire source object.
151
+ * Supports both sync and async functions.
152
+ */
153
+ compute(fn: ((obj: InferType<TFromSchema>) => SchemaPropertyInferredType<TToSchema, TKey>) | ((obj: InferType<TFromSchema>) => Promise<SchemaPropertyInferredType<TToSchema, TKey>>)): Mapper<TFromSchema, TToSchema, Exclude<TUnmapped, TKey>, TRegistered>;
154
+ /**
155
+ * Explicitly excludes the target property from mapping.
156
+ * The property will not appear in the output object.
157
+ */
158
+ ignore(): Mapper<TFromSchema, TToSchema, Exclude<TUnmapped, TKey>, TRegistered>;
159
+ }
160
+ /**
161
+ * A fluent builder for configuring how each target property is populated
162
+ * from a source schema. Uses PropertyDescriptors as pointers to properties
163
+ * (similar to expressions in C# .NET).
164
+ *
165
+ * The `TUnmapped` type parameter tracks which target properties have not
166
+ * yet been mapped or ignored. `getMapper()` is only callable (without
167
+ * arguments) when `TUnmapped` is `never` — i.e. all properties have been
168
+ * accounted for. If any property is missing, TypeScript will produce a
169
+ * compile-time type error (a type-assignability mismatch that includes
170
+ * the unmapped property names in its type parameters).
171
+ *
172
+ * @typeParam TFromSchema - source ObjectSchemaBuilder
173
+ * @typeParam TToSchema - target ObjectSchemaBuilder
174
+ * @typeParam TUnmapped - union of target property key names not yet mapped
175
+ */
176
+ export declare class Mapper<TFromSchema extends ObjectSchemaBuilder<any, any, any>, TToSchema extends ObjectSchemaBuilder<any, any, any>, TUnmapped extends string = SchemaKeys<TToSchema>, TRegistered = never> {
177
+ /** Phantom property for structural type-checking of TUnmapped. */
178
+ readonly [SYMBOL_UNMAPPED]: TUnmapped;
179
+ private readonly _fromSchema;
180
+ private readonly _toSchema;
181
+ private readonly _registry;
182
+ private readonly _mappings;
183
+ /**
184
+ * Creates a new instance of the Mapper class.
185
+ * @param fromSchema - `object` schema to map from
186
+ * @param toSchema - `object` schema to map to
187
+ * @param registry - optional MappingRegistry to auto-register the mapper
188
+ */
189
+ constructor(fromSchema: TFromSchema, toSchema: TToSchema, registry?: MappingRegistry<any>);
190
+ /**
191
+ * Selects a target property to configure. The selector callback
192
+ * receives a tree of all target properties. Navigate by
193
+ * property name: `(t) => t.cityName`.
194
+ *
195
+ * Auto-mappable properties (same name and compatible type, or
196
+ * ObjectSchemaBuilder with a registered mapping) are also available
197
+ * for explicit override.
198
+ */
199
+ for<TKey extends SchemaKeys<TToSchema>>(selector: (tree: TargetPropertyTree<TToSchema, SchemaKeys<TToSchema>>) => TargetPropertyKey<TKey>): PropertyMappingBuilder<TFromSchema, TToSchema, TKey, TUnmapped, TRegistered>;
200
+ /**
201
+ * Returns the configured async mapping function.
202
+ *
203
+ * **Compile-time safety:** This method is only callable without
204
+ * arguments when all target properties have been mapped or explicitly
205
+ * ignored. If any property is unmapped, TypeScript will require
206
+ * a string argument describing the unmapped properties, producing
207
+ * a clear compile-time error.
208
+ *
209
+ * **Runtime safety:** Even if TypeScript checks are bypassed (e.g.
210
+ * via `as any`), a `MapperConfigurationError` is thrown at runtime
211
+ * listing the unmapped properties.
212
+ */
213
+ getMapper(..._args: [TUnmapped] extends [never] ? [] : [error: `Unmapped properties: ${TUnmapped}`]): SchemaToSchemaMapperResult<TFromSchema, TToSchema>;
214
+ }
215
+ export declare class MappingRegistry<TRegistered = never> {
216
+ #private;
217
+ protected readonly _mappers: Map<ObjectSchemaBuilder<any, any, any>, Map<ObjectSchemaBuilder<any, any, any>, SchemaToSchemaMapperResult<any, any>>>;
218
+ /**
219
+ * Defines a mapping between two schemas and returns a new immutable
220
+ * registry containing the mapping. The callback `fn` receives a fresh
221
+ * `Mapper` and must return it after configuring property mappings.
222
+ * The mapper is automatically finalized and registered. Properties
223
+ * not explicitly mapped or ignored may be auto-mapped if a matching
224
+ * nested mapping is already registered in the registry.
225
+ *
226
+ * @param fromSchema - source ObjectSchemaBuilder
227
+ * @param toSchema - target ObjectSchemaBuilder
228
+ * @param fn - callback that configures property mappings on the mapper
229
+ * @returns a new MappingRegistry containing all previous mappings plus
230
+ * the newly configured one
231
+ * @throws if schemas are invalid, mapping is duplicate, or unmapped
232
+ * properties remain that cannot be auto-mapped
233
+ */
234
+ configure<TFromSchema extends ObjectSchemaBuilder<any, any, any>, TToSchema extends ObjectSchemaBuilder<any, any, any>>(fromSchema: TFromSchema, toSchema: TToSchema, fn: (mapper: Mapper<TFromSchema, TToSchema, KeysNeedingMapping<TFromSchema, TToSchema, TRegistered>, TRegistered>) => Mapper<TFromSchema, TToSchema, never, TRegistered>): MappingRegistry<TRegistered | [TFromSchema, TToSchema]>;
235
+ /**
236
+ * Gets a mapper function that will map from the fromSchema to the toSchema.
237
+ * Throws an error if no mapper is found for the given schemas pair.
238
+ * @param fromSchema a schema to map from
239
+ * @param toSchema a schema to map to
240
+ * @returns a function that will take a value of the fromSchema type as an argument and
241
+ * return a promise resolving to a value of the toSchema type
242
+ */
243
+ getMapper<TFromSchema extends ObjectSchemaBuilder<any, any, any>, TToSchema extends ObjectSchemaBuilder<any, any, any>>(fromSchema: TFromSchema, toSchema: TToSchema): SchemaToSchemaMapperResult<TFromSchema, TToSchema>;
244
+ }
245
+ /**
246
+ * Creates a new empty {@link MappingRegistry}.
247
+ *
248
+ * This is a convenience factory function — an alternative to
249
+ * `new MappingRegistry()` that reads better in a fluent chain:
250
+ *
251
+ * ```ts
252
+ * const registry = mapper()
253
+ * .configure(A, B, m => ...)
254
+ * .configure(C, D, m => ...);
255
+ * ```
256
+ */
257
+ export declare function mapper(): MappingRegistry;
258
+ export {};
@@ -0,0 +1,2 @@
1
+ export type { SchemaToSchemaMapperResult } from './MappingRegistry.js';
2
+ export { Mapper, MapperConfigurationError, MappingRegistry, mapper, PropertyMappingBuilder } from './MappingRegistry.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{ArraySchemaBuilder as u,ObjectSchemaBuilder as i,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as g}from"@cleverbrush/schema";var j=Symbol("targetPropertyKey");var l=class extends Error{constructor(e){super(typeof e=="string"?e:`Mapper configuration error: the following target properties are not mapped and not ignored: ${e.join(", ")}`),this.name="MapperConfigurationError"}};function b(m,e,r){if(m instanceof i&&e instanceof i){if(r){let o=r._mappers.get(m)?.get(e);if(o)return o}let a=Object.keys(m.introspect().properties||{}).sort(),s=Object.keys(e.introspect().properties||{}).sort();return a.length===s.length&&a.every((t,o)=>t===s[o])?async t=>t:null}if(m instanceof u&&e instanceof u){let a=m.introspect().elementSchema,s=e.introspect().elementSchema;if(a&&s){let t=b(a,s,r);if(t)return async o=>{if(o!=null)return Array.isArray(o)?Promise.all(o.map(t)):o}}return null}if(!(m instanceof i)&&!(e instanceof i)&&!(m instanceof u)&&!(e instanceof u)){let a=typeof m.introspect=="function"?m.introspect():void 0,s=typeof e.introspect=="function"?e.introspect():void 0;return!a||!s||a.type!==s.type?null:async t=>t}return null}var x=class{_mapper;_targetKey;constructor(e,r){this._mapper=e,this._targetKey=r}from(e,...r){let a=i.getPropertiesFor(this._mapper._fromSchema),t=e(a)[g],o=t.getSchema(),y=(this._mapper._toSchema.introspect().properties||{})[this._targetKey];if(o instanceof i&&y instanceof i&&this._mapper._registry){let T=this._mapper._registry._mappers.get(o)?.get(y);if(T)return this._mapper._mappings.set(this._targetKey,{type:"auto",sourceDescriptorInner:t,autoMapper:T}),this._mapper}if(o instanceof u&&y instanceof u){let h=o.introspect().elementSchema,T=y.introspect().elementSchema;if(h&&T){let p=b(h,T,this._mapper._registry);if(p)return this._mapper._mappings.set(this._targetKey,{type:"autoArray",sourceDescriptorInner:t,elementMapper:p}),this._mapper}}return this._mapper._mappings.set(this._targetKey,{type:"prop",sourceDescriptorInner:t}),this._mapper}compute(e){return this._mapper._mappings.set(this._targetKey,{type:"custom",fn:e}),this._mapper}ignore(){return this._mapper._mappings.set(this._targetKey,{type:"ignore"}),this._mapper}},M=class{_fromSchema;_toSchema;_registry;_mappings=new Map;constructor(e,r,a){this._fromSchema=e,this._toSchema=r,this._registry=a}for(e){let r,a=new Proxy({},{get(o,d){return typeof d=="string"&&(r=d),{[j]:d}}});if(e(a),!r)throw new Error("for selector must access a property on the target tree");let s=this._toSchema.introspect();if(!(s.properties?Object.keys(s.properties):[]).includes(r))throw new l(`Property "${r}" does not exist in the target schema`);return new x(this,r)}getMapper(...e){let r=this._toSchema.introspect(),a=r.properties?Object.keys(r.properties):[];if(this._registry){let h=this._fromSchema.introspect().properties||{},T=r.properties||{},p=i.getPropertiesFor(this._fromSchema);for(let c of a){if(this._mappings.has(c))continue;let S=h[c],n=T[c];if(!S||!n)continue;let f=p[c];if(f?.[g]){if(S instanceof i&&n instanceof i){let _=this._registry._mappers.get(S)?.get(n);if(_){this._mappings.set(c,{type:"auto",sourceDescriptorInner:f[g],autoMapper:_});continue}let P=Object.keys(S.introspect().properties||{}).sort(),B=Object.keys(n.introspect().properties||{}).sort();P.length===B.length&&P.every((O,E)=>O===B[E])&&this._mappings.set(c,{type:"prop",sourceDescriptorInner:f[g]});continue}if(!(S instanceof i)&&!(n instanceof i)&&!(S instanceof u)&&!(n instanceof u)){this._mappings.set(c,{type:"prop",sourceDescriptorInner:f[g]});continue}if(S instanceof u&&n instanceof u){let K=S.introspect().elementSchema,_=n.introspect().elementSchema;if(K&&_){let P=b(K,_,this._registry);P&&this._mappings.set(c,{type:"autoArray",sourceDescriptorInner:f[g],elementMapper:P})}}}}}let s=a.filter(y=>!this._mappings.has(y));if(s.length>0)throw new l(s);let t=i.getPropertiesFor(this._toSchema),o=new Map(this._mappings),d=async y=>{let h={};for(let[T,p]of o){if(p.type==="ignore")continue;let c;if(p.type==="prop"){let n=p.sourceDescriptorInner.getValue(y);if(n.success)c=n.value;else continue}else if(p.type==="custom")c=await p.fn(y);else if(p.type==="auto"){let n=p.sourceDescriptorInner.getValue(y);if(n.success&&n.value!==void 0)c=await p.autoMapper(n.value);else continue}else if(p.type==="autoArray"){let n=p.sourceDescriptorInner.getValue(y);if(n.success&&n.value!=null){if(!Array.isArray(n.value))throw new l(`Expected array for property "${T}" but got ${typeof n.value}`);c=await Promise.all(n.value.map(p.elementMapper))}else continue}let S=t[T];S?.[g]?S[g].setValue(h,c,{createMissingStructure:!0}):h[T]=c}return h};return this._registry&&this._registry._mappers.get(this._fromSchema)?.set(this._toSchema,d),d}},R=class m{_mappers=new Map;#e(e,r){return!(!e||!r||!(e instanceof i)||!(r instanceof i))}configure(e,r,a){if(!this.#e(e,r))throw new Error("Both fromSchema and toSchema must be instances of ObjectSchemaBuilder");if(this._mappers.get(e)?.has(r))throw new Error("Duplicate mapping: a mapping for this schemas pair is already registered");let t=new m;for(let[y,h]of this._mappers)t._mappers.set(y,new Map(h));t._mappers.has(e)||t._mappers.set(e,new Map);let o=new M(e,r,t);return a(o).getMapper(),t}getMapper(e,r){if(!this.#e(e,r))throw new Error("Both fromSchema and toSchema must be instances of ObjectSchemaBuilder");let a=this._mappers.get(e);if(!a)throw new Error("No mapper found for the given schemas pair");let s=a.get(r);if(!s)throw new Error("No mapper found for the given schemas pair");return s}};function I(){return new R}export{M as Mapper,l as MapperConfigurationError,R as MappingRegistry,x as PropertyMappingBuilder,I as mapper};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/MappingRegistry.ts"],"sourcesContent":["import {\n ArraySchemaBuilder,\n type InferType,\n ObjectSchemaBuilder,\n type PropertyDescriptor,\n type PropertyDescriptorTree,\n type SchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\n\n/**\n * Internal symbol used to brand target property descriptors with their key.\n * Using a symbol instead of a string property keeps it out of IntelliSense.\n */\nconst SYMBOL_TARGET_PROPERTY_KEY: unique symbol = Symbol('targetPropertyKey');\n\n/**\n * Internal symbol used as a phantom property key on Mapper to track\n * unmapped target properties. Using a symbol keeps it out of IntelliSense.\n */\nconst SYMBOL_UNMAPPED: unique symbol = Symbol('unmapped');\n\n// ── Helper Types ──────────────────────────────────────────────────────\n\n/**\n * Extracts the properties record from an ObjectSchemaBuilder.\n */\ntype ExtractSchemaProperties<T> =\n T extends ObjectSchemaBuilder<infer TProperties, any, any>\n ? TProperties\n : never;\n\n/**\n * Gets all top-level property key names of an ObjectSchemaBuilder.\n */\ntype SchemaKeys<T extends ObjectSchemaBuilder<any, any, any>> =\n keyof ExtractSchemaProperties<T> & string;\n\n/**\n * Branded phantom type that tags a property descriptor with its key name.\n * This allows TypeScript to infer which property was selected in the\n * `for` callback, enabling compile-time tracking of mapped vs unmapped\n * properties.\n */\ntype TargetPropertyKey<K extends string> = {\n readonly [SYMBOL_TARGET_PROPERTY_KEY]: K;\n};\n\n/**\n * Creates a tree of selectable target properties, filtered to only show\n * properties whose keys are in `TAllowedKeys`. Each property is branded\n * with `TargetPropertyKey<K>` so the key can be inferred from the return\n * type of the selector callback.\n */\ntype TargetPropertyTree<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TAllowedKeys extends string\n> = {\n [K in SchemaKeys<TSchema> & TAllowedKeys]: TargetPropertyKey<K> &\n PropertyDescriptor<TSchema, ExtractSchemaProperties<TSchema>[K], any>;\n};\n\n/**\n * Infers the TypeScript type of a specific property in an ObjectSchemaBuilder\n * by its key name.\n */\ntype SchemaPropertyInferredType<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n K extends string\n> = K extends keyof ExtractSchemaProperties<TSchema>\n ? InferType<ExtractSchemaProperties<TSchema>[K]>\n : never;\n\n/**\n * Extracts the schema (SchemaBuilder) of a specific property in an\n * ObjectSchemaBuilder by its key name.\n */\ntype TargetPropertySchema<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n K extends string\n> = K extends keyof ExtractSchemaProperties<TSchema>\n ? ExtractSchemaProperties<TSchema>[K]\n : never;\n\n/**\n * Extracts the element schema from an ArraySchemaBuilder.\n */\ntype ExtractArrayElementSchema<T> =\n T extends ArraySchemaBuilder<infer TElementSchema, any, any>\n ? TElementSchema\n : never;\n\n/**\n * Extracts the property schema from a PropertyDescriptor's `getSchema()`\n * return type. Used by {@link IsFromCompatible} to recover the source\n * property's schema from the inferred `TReturn` of the `from()` selector.\n */\ntype ExtractPropertySchema<T> = T extends {\n [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {\n getSchema(): infer TSchema;\n };\n}\n ? TSchema\n : never;\n\n/**\n * Determines whether a property needs an explicit mapping configuration.\n *\n * - Both are ObjectSchemaBuilder with registered mapping → `false` (auto-mappable)\n * - Both are ObjectSchemaBuilder without registered mapping → `true`\n * - Either is ObjectSchemaBuilder but the other is not → `true`\n * - Both are ArraySchemaBuilder with element schemas that have registered mapping → `false`\n * - Both are ArraySchemaBuilder with same InferType → `false`\n * - Array vs non-array → `true`\n * - Neither is ObjectSchemaBuilder + InferType<source> extends InferType<target>\n * → `false` (same-name, compatible primitive types → auto-mappable)\n * - Neither is ObjectSchemaBuilder + incompatible InferType → `true`\n */\ntype NeedsMapping<TSourcePropSchema, TTargetPropSchema, TRegistered> =\n TSourcePropSchema extends ArraySchemaBuilder<any, any, any>\n ? TTargetPropSchema extends ArraySchemaBuilder<any, any, any>\n ? NeedsMapping<\n ExtractArrayElementSchema<TSourcePropSchema>,\n ExtractArrayElementSchema<TTargetPropSchema>,\n TRegistered\n >\n : true\n : TTargetPropSchema extends ArraySchemaBuilder<any, any, any>\n ? true\n : TSourcePropSchema extends ObjectSchemaBuilder<any, any, any>\n ? TTargetPropSchema extends ObjectSchemaBuilder<any, any, any>\n ? [TSourcePropSchema, TTargetPropSchema] extends TRegistered\n ? false\n : InferType<TSourcePropSchema> extends InferType<TTargetPropSchema>\n ? InferType<TTargetPropSchema> extends InferType<TSourcePropSchema>\n ? false\n : true\n : true\n : true\n : TTargetPropSchema extends ObjectSchemaBuilder<any, any, any>\n ? true\n : InferType<TSourcePropSchema> extends InferType<TTargetPropSchema>\n ? false\n : true;\n\n/**\n * From all keys of the target schema, filter down to only those that\n * require explicit mapping (i.e. `NeedsMapping` is `true`).\n * Keys where `NeedsMapping` is `false` can be auto-mapped via the registry.\n */\ntype KeysNeedingMapping<\n TFromSchema extends ObjectSchemaBuilder<any, any, any>,\n TToSchema extends ObjectSchemaBuilder<any, any, any>,\n TRegistered\n> = {\n [K in SchemaKeys<TToSchema>]: K extends SchemaKeys<TFromSchema>\n ? NeedsMapping<\n ExtractSchemaProperties<TFromSchema>[K],\n ExtractSchemaProperties<TToSchema>[K],\n TRegistered\n > extends true\n ? K\n : never\n : K;\n}[SchemaKeys<TToSchema>];\n\n/**\n * Checks whether two property schemas are compatible, considering\n * registered mappings. Mirrors {@link NeedsMapping}'s structure:\n * receives schemas as direct type parameters, uses\n * {@link ExtractArrayElementSchema} for arrays, and checks registration\n * via tuple-extends-union (not inline `infer`).\n */\ntype CheckSchemaCompatible<TSourceSchema, TTargetSchema, TRegistered> =\n // Direct InferType match → compatible\n [InferType<TSourceSchema>] extends [InferType<TTargetSchema>]\n ? true\n : // Both arrays → recurse into element schemas\n TSourceSchema extends ArraySchemaBuilder<any, any, any>\n ? TTargetSchema extends ArraySchemaBuilder<any, any, any>\n ? CheckSchemaCompatible<\n ExtractArrayElementSchema<TSourceSchema>,\n ExtractArrayElementSchema<TTargetSchema>,\n TRegistered\n >\n : false\n : // Both objects → check registration\n TSourceSchema extends ObjectSchemaBuilder<any, any, any>\n ? TTargetSchema extends ObjectSchemaBuilder<any, any, any>\n ? [TSourceSchema, TTargetSchema] extends TRegistered\n ? true\n : false\n : false\n : false;\n\n/**\n * Checks whether the source property selected by `from()` is compatible\n * with the target property, evaluated *after* `TReturn` has been inferred.\n *\n * This decouples inference from validation: the `TReturn` constraint uses\n * `any` for setValue/getValue value types so TypeScript always infers\n * successfully, and this type performs the actual compatibility check\n * in the `_args` conditional spread.\n */\ntype IsFromCompatible<\n TReturn,\n TToSchema extends ObjectSchemaBuilder<any, any, any>,\n TKey extends string,\n TRegistered\n> = [ExtractPropertySchema<TReturn>] extends [never]\n ? false\n : CheckSchemaCompatible<\n ExtractPropertySchema<TReturn>,\n TargetPropertySchema<TToSchema, TKey>,\n TRegistered\n >;\n\n// ── Mapper Result Type ────────────────────────────────────────────────\n\nexport type SchemaToSchemaMapperResult<\n TFromSchema extends ObjectSchemaBuilder<any, any, any>,\n TToSchema extends ObjectSchemaBuilder<any, any, any>\n> = (from: InferType<TFromSchema>) => Promise<InferType<TToSchema>>;\n\n// ── Error Class ───────────────────────────────────────────────────────\n\nexport class MapperConfigurationError extends Error {\n constructor(messageOrUnmappedProperties: string | string[]) {\n super(\n typeof messageOrUnmappedProperties === 'string'\n ? messageOrUnmappedProperties\n : `Mapper configuration error: the following target properties are not mapped and not ignored: ${messageOrUnmappedProperties.join(', ')}`\n );\n this.name = 'MapperConfigurationError';\n }\n}\n\n// ── Internal Mapping Entry ────────────────────────────────────────────\n\ntype MappingEntry = {\n type: 'prop' | 'custom' | 'ignore' | 'auto' | 'autoArray';\n sourceDescriptorInner?: ReturnType<\n PropertyDescriptor<\n any,\n any,\n any\n >[typeof SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]['getValue']\n > extends any\n ? any\n : never;\n fn?: (obj: any) => any;\n autoMapper?: SchemaToSchemaMapperResult<any, any>;\n elementMapper?: (element: any) => Promise<any>;\n};\n\n// ── Array Element Mapper Resolution ───────────────────────────────────\n\n/**\n * Resolves an element mapper for array properties.\n * Handles ObjectSchemaBuilder elements (via registry) and\n * primitive elements (direct copy when types match).\n * Returns null if no valid element mapper can be resolved.\n */\nfunction resolveElementMapper(\n sourceElementSchema: SchemaBuilder<any, any, any>,\n targetElementSchema: SchemaBuilder<any, any, any>,\n registry: MappingRegistry<any> | undefined\n): ((element: any) => Promise<any>) | null {\n // Both element schemas are ObjectSchemaBuilder: look up registered mapper\n if (\n sourceElementSchema instanceof ObjectSchemaBuilder &&\n targetElementSchema instanceof ObjectSchemaBuilder\n ) {\n if (registry) {\n const fromSchemaMappers =\n registry['_mappers'].get(sourceElementSchema);\n const autoMapper = fromSchemaMappers?.get(targetElementSchema);\n if (autoMapper) {\n return autoMapper as (element: any) => Promise<any>;\n }\n }\n // Same inferred structure: copy directly\n const fromKeys = Object.keys(\n sourceElementSchema.introspect().properties || {}\n ).sort();\n const toKeys = Object.keys(\n targetElementSchema.introspect().properties || {}\n ).sort();\n if (\n fromKeys.length === toKeys.length &&\n fromKeys.every((k, i) => k === toKeys[i])\n ) {\n return async (element: any) => element;\n }\n return null;\n }\n\n // Both element schemas are ArraySchemaBuilder: recursive element mapping\n if (\n sourceElementSchema instanceof ArraySchemaBuilder &&\n targetElementSchema instanceof ArraySchemaBuilder\n ) {\n const innerSourceElement =\n sourceElementSchema.introspect().elementSchema;\n const innerTargetElement =\n targetElementSchema.introspect().elementSchema;\n if (innerSourceElement && innerTargetElement) {\n const innerMapper = resolveElementMapper(\n innerSourceElement,\n innerTargetElement,\n registry\n );\n if (innerMapper) {\n return async (arr: any) => {\n if (arr == null) return undefined;\n if (!Array.isArray(arr)) return arr;\n return Promise.all(arr.map(innerMapper));\n };\n }\n }\n return null;\n }\n\n // Neither is ObjectSchemaBuilder nor ArraySchemaBuilder:\n // treat as primitive-compatible only when element schema types match.\n if (\n !(sourceElementSchema instanceof ObjectSchemaBuilder) &&\n !(targetElementSchema instanceof ObjectSchemaBuilder) &&\n !(sourceElementSchema instanceof ArraySchemaBuilder) &&\n !(targetElementSchema instanceof ArraySchemaBuilder)\n ) {\n const sourceIntrospection =\n typeof (sourceElementSchema as any).introspect === 'function'\n ? (sourceElementSchema as any).introspect()\n : undefined;\n const targetIntrospection =\n typeof (targetElementSchema as any).introspect === 'function'\n ? (targetElementSchema as any).introspect()\n : undefined;\n\n if (\n !sourceIntrospection ||\n !targetIntrospection ||\n sourceIntrospection.type !== targetIntrospection.type\n ) {\n return null;\n }\n return async (element: any) => element;\n }\n\n return null;\n}\n\n// ── PropertyMappingBuilder ────────────────────────────────────────────\n\n/**\n * Intermediate builder returned by `for()`. Provides three strategies\n * to configure how the selected target property is populated:\n * - `from()` — copy from a source property\n * - `compute()` — compute from the entire source object\n * - `ignore()` — explicitly skip the property\n */\nexport class PropertyMappingBuilder<\n TFromSchema extends ObjectSchemaBuilder<any, any, any>,\n TToSchema extends ObjectSchemaBuilder<any, any, any>,\n TKey extends string,\n TUnmapped extends string,\n TRegistered = never\n> {\n private readonly _mapper: Mapper<TFromSchema, TToSchema, any, TRegistered>;\n private readonly _targetKey: TKey;\n\n /** @internal */\n constructor(\n mapper: Mapper<TFromSchema, TToSchema, any, TRegistered>,\n targetKey: TKey\n ) {\n this._mapper = mapper;\n this._targetKey = targetKey;\n }\n\n /**\n * Maps the target property from a source property. The selector\n * receives the source schema's PropertyDescriptorTree and supports\n * nested paths (e.g. `(s) => s.address.city`).\n *\n * Type compatibility is enforced: only source properties whose\n * inferred type is assignable to the target property type will\n * appear in the selector callback.\n *\n * Under `strictFunctionTypes`, the `setValue` and `getValue` constraints\n * provide bidirectional type checking:\n * - `setValue` contravariance rejects source schemas with extra properties\n * - `getValue` covariance rejects source schemas with missing properties\n * - registered mappings widen the constraint via intersection\n */\n public from<\n TReturn extends {\n [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {\n getSchema(): SchemaBuilder<any, any, any>;\n setValue: (obj: any, value: any) => any;\n getValue: (obj: any) => {\n value?: any;\n success: boolean;\n };\n };\n }\n >(\n selector: (\n tree: PropertyDescriptorTree<\n TFromSchema,\n TFromSchema,\n SchemaPropertyInferredType<TToSchema, TKey>\n >\n ) => TReturn,\n ..._args: [TReturn] extends [never]\n ? [\n error: `Property '${TKey}': source property type is not assignable to the target property type. Use compute() instead.`\n ]\n : IsFromCompatible<\n TReturn,\n TToSchema,\n TKey,\n TRegistered\n > extends true\n ? []\n : [\n error: `Property '${TKey}': source property type is not assignable to the target property type. Use compute() instead.`\n ]\n ): Mapper<TFromSchema, TToSchema, Exclude<TUnmapped, TKey>, TRegistered> {\n const sourceTree = ObjectSchemaBuilder.getPropertiesFor(\n this._mapper['_fromSchema']\n );\n const sourceDescriptor = selector(sourceTree as any);\n const inner = sourceDescriptor[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR];\n\n // Check if both source and target property schemas are ObjectSchemaBuilder;\n // if so, look up the registered mapper and use it at runtime.\n const sourceSchema = inner.getSchema();\n const toProperties = (this._mapper['_toSchema'].introspect()\n .properties || {}) as Record<string, any>;\n const targetPropSchema = toProperties[this._targetKey];\n\n if (\n sourceSchema instanceof ObjectSchemaBuilder &&\n targetPropSchema instanceof ObjectSchemaBuilder &&\n this._mapper['_registry']\n ) {\n const fromSchemaMappers =\n this._mapper['_registry']['_mappers'].get(sourceSchema);\n const autoMapper = fromSchemaMappers?.get(targetPropSchema);\n if (autoMapper) {\n this._mapper['_mappings'].set(this._targetKey, {\n type: 'auto',\n sourceDescriptorInner: inner,\n autoMapper\n });\n return this._mapper as any;\n }\n }\n\n // Check if both source and target are ArraySchemaBuilder;\n // if so, resolve the element mapper and use element-wise mapping.\n if (\n sourceSchema instanceof ArraySchemaBuilder &&\n targetPropSchema instanceof ArraySchemaBuilder\n ) {\n const sourceElementSchema = (\n sourceSchema as ArraySchemaBuilder<any, any, any>\n ).introspect().elementSchema;\n const targetElementSchema = (\n targetPropSchema as ArraySchemaBuilder<any, any, any>\n ).introspect().elementSchema;\n\n if (sourceElementSchema && targetElementSchema) {\n const elementMapper = resolveElementMapper(\n sourceElementSchema,\n targetElementSchema,\n this._mapper['_registry']\n );\n if (elementMapper) {\n this._mapper['_mappings'].set(this._targetKey, {\n type: 'autoArray',\n sourceDescriptorInner: inner,\n elementMapper\n });\n return this._mapper as any;\n }\n }\n }\n\n this._mapper['_mappings'].set(this._targetKey, {\n type: 'prop',\n sourceDescriptorInner: inner\n });\n\n return this._mapper as any;\n }\n\n /**\n * Computes the target property value from the entire source object.\n * Supports both sync and async functions.\n */\n public compute(\n fn:\n | ((\n obj: InferType<TFromSchema>\n ) => SchemaPropertyInferredType<TToSchema, TKey>)\n | ((\n obj: InferType<TFromSchema>\n ) => Promise<SchemaPropertyInferredType<TToSchema, TKey>>)\n ): Mapper<TFromSchema, TToSchema, Exclude<TUnmapped, TKey>, TRegistered> {\n this._mapper['_mappings'].set(this._targetKey, {\n type: 'custom',\n fn: fn as any\n });\n\n return this._mapper as any;\n }\n\n /**\n * Explicitly excludes the target property from mapping.\n * The property will not appear in the output object.\n */\n public ignore(): Mapper<\n TFromSchema,\n TToSchema,\n Exclude<TUnmapped, TKey>,\n TRegistered\n > {\n this._mapper['_mappings'].set(this._targetKey, {\n type: 'ignore'\n });\n\n return this._mapper as any;\n }\n}\n\n// ── Mapper ────────────────────────────────────────────────────────────\n\n/**\n * A fluent builder for configuring how each target property is populated\n * from a source schema. Uses PropertyDescriptors as pointers to properties\n * (similar to expressions in C# .NET).\n *\n * The `TUnmapped` type parameter tracks which target properties have not\n * yet been mapped or ignored. `getMapper()` is only callable (without\n * arguments) when `TUnmapped` is `never` — i.e. all properties have been\n * accounted for. If any property is missing, TypeScript will produce a\n * compile-time type error (a type-assignability mismatch that includes\n * the unmapped property names in its type parameters).\n *\n * @typeParam TFromSchema - source ObjectSchemaBuilder\n * @typeParam TToSchema - target ObjectSchemaBuilder\n * @typeParam TUnmapped - union of target property key names not yet mapped\n */\nexport class Mapper<\n TFromSchema extends ObjectSchemaBuilder<any, any, any>,\n TToSchema extends ObjectSchemaBuilder<any, any, any>,\n TUnmapped extends string = SchemaKeys<TToSchema>,\n TRegistered = never\n> {\n /** Phantom property for structural type-checking of TUnmapped. */\n declare readonly [SYMBOL_UNMAPPED]: TUnmapped;\n\n private readonly _fromSchema: TFromSchema;\n private readonly _toSchema: TToSchema;\n private readonly _registry: MappingRegistry<any> | undefined;\n private readonly _mappings: Map<string, MappingEntry> = new Map();\n\n /**\n * Creates a new instance of the Mapper class.\n * @param fromSchema - `object` schema to map from\n * @param toSchema - `object` schema to map to\n * @param registry - optional MappingRegistry to auto-register the mapper\n */\n public constructor(\n fromSchema: TFromSchema,\n toSchema: TToSchema,\n registry?: MappingRegistry<any>\n ) {\n this._fromSchema = fromSchema;\n this._toSchema = toSchema;\n this._registry = registry;\n }\n\n /**\n * Selects a target property to configure. The selector callback\n * receives a tree of all target properties. Navigate by\n * property name: `(t) => t.cityName`.\n *\n * Auto-mappable properties (same name and compatible type, or\n * ObjectSchemaBuilder with a registered mapping) are also available\n * for explicit override.\n */\n public for<TKey extends SchemaKeys<TToSchema>>(\n selector: (\n tree: TargetPropertyTree<TToSchema, SchemaKeys<TToSchema>>\n ) => TargetPropertyKey<TKey>\n ): PropertyMappingBuilder<\n TFromSchema,\n TToSchema,\n TKey,\n TUnmapped,\n TRegistered\n > {\n // At runtime, use a Proxy to detect which property was accessed\n let capturedKey: string | undefined;\n const proxy = new Proxy({} as any, {\n get(_target, prop) {\n if (typeof prop === 'string') {\n capturedKey = prop;\n }\n return { [SYMBOL_TARGET_PROPERTY_KEY]: prop };\n }\n });\n\n selector(proxy);\n\n if (!capturedKey) {\n throw new Error(\n 'for selector must access a property on the target tree'\n );\n }\n\n // Validate that the captured key exists in the target schema\n const toIntrospection = this._toSchema.introspect();\n const targetProperties = toIntrospection.properties\n ? Object.keys(toIntrospection.properties)\n : [];\n if (!targetProperties.includes(capturedKey)) {\n throw new MapperConfigurationError(\n `Property \"${capturedKey}\" does not exist in the target schema`\n );\n }\n\n return new PropertyMappingBuilder(this, capturedKey as TKey);\n }\n\n /**\n * Returns the configured async mapping function.\n *\n * **Compile-time safety:** This method is only callable without\n * arguments when all target properties have been mapped or explicitly\n * ignored. If any property is unmapped, TypeScript will require\n * a string argument describing the unmapped properties, producing\n * a clear compile-time error.\n *\n * **Runtime safety:** Even if TypeScript checks are bypassed (e.g.\n * via `as any`), a `MapperConfigurationError` is thrown at runtime\n * listing the unmapped properties.\n */\n public getMapper(\n ..._args: [TUnmapped] extends [never]\n ? []\n : [error: `Unmapped properties: ${TUnmapped}`]\n ): SchemaToSchemaMapperResult<TFromSchema, TToSchema> {\n // Runtime validation: ensure all target properties are covered\n const toIntrospection = this._toSchema.introspect();\n const targetProperties = toIntrospection.properties\n ? Object.keys(toIntrospection.properties)\n : [];\n\n // Auto-mapping: fill in unmapped properties using registered nested mappers\n if (this._registry) {\n const fromIntrospection = this._fromSchema.introspect();\n const fromProperties = (fromIntrospection.properties ||\n {}) as Record<string, any>;\n const toProperties = (toIntrospection.properties || {}) as Record<\n string,\n any\n >;\n const fromTree = ObjectSchemaBuilder.getPropertiesFor(\n this._fromSchema\n );\n\n for (const key of targetProperties) {\n if (this._mappings.has(key)) continue;\n\n const fromPropSchema = fromProperties[key];\n const toPropSchema = toProperties[key];\n\n if (!fromPropSchema || !toPropSchema) continue;\n\n const sourceDescriptor = (fromTree as any)[key];\n if (!sourceDescriptor?.[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR])\n continue;\n\n // ObjectSchemaBuilder → ObjectSchemaBuilder: use registered mapper\n if (\n fromPropSchema instanceof ObjectSchemaBuilder &&\n toPropSchema instanceof ObjectSchemaBuilder\n ) {\n const fromSchemaMappers =\n this._registry['_mappers'].get(fromPropSchema);\n const autoMapper = fromSchemaMappers?.get(toPropSchema);\n if (autoMapper) {\n this._mappings.set(key, {\n type: 'auto',\n sourceDescriptorInner:\n sourceDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ],\n autoMapper\n });\n continue;\n }\n\n // Same-name ObjectSchemaBuilder with identical property keys:\n // copy directly (full type safety ensured at compile time\n // via bidirectional InferType check)\n const fromKeys = Object.keys(\n fromPropSchema.introspect().properties || {}\n ).sort();\n const toKeys = Object.keys(\n toPropSchema.introspect().properties || {}\n ).sort();\n if (\n fromKeys.length === toKeys.length &&\n fromKeys.every((k, i) => k === toKeys[i])\n ) {\n this._mappings.set(key, {\n type: 'prop',\n sourceDescriptorInner:\n sourceDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ]\n });\n }\n continue;\n }\n\n // Primitive → Primitive: auto-map same-name props\n // (type compatibility is ensured at the type level by\n // KeysNeedingMapping / NeedsMapping)\n if (\n !(fromPropSchema instanceof ObjectSchemaBuilder) &&\n !(toPropSchema instanceof ObjectSchemaBuilder) &&\n !(fromPropSchema instanceof ArraySchemaBuilder) &&\n !(toPropSchema instanceof ArraySchemaBuilder)\n ) {\n this._mappings.set(key, {\n type: 'prop',\n sourceDescriptorInner:\n sourceDescriptor[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]\n });\n continue;\n }\n\n // ArraySchemaBuilder → ArraySchemaBuilder: use element mapper\n if (\n fromPropSchema instanceof ArraySchemaBuilder &&\n toPropSchema instanceof ArraySchemaBuilder\n ) {\n const sourceElementSchema =\n fromPropSchema.introspect().elementSchema;\n const targetElementSchema =\n toPropSchema.introspect().elementSchema;\n if (sourceElementSchema && targetElementSchema) {\n const elementMapper = resolveElementMapper(\n sourceElementSchema,\n targetElementSchema,\n this._registry\n );\n if (elementMapper) {\n this._mappings.set(key, {\n type: 'autoArray',\n sourceDescriptorInner:\n sourceDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ],\n elementMapper\n });\n }\n }\n }\n }\n }\n\n const unmapped = targetProperties.filter(\n key => !this._mappings.has(key)\n );\n\n if (unmapped.length > 0) {\n throw new MapperConfigurationError(unmapped);\n }\n\n const targetTree = ObjectSchemaBuilder.getPropertiesFor(this._toSchema);\n\n const mappings = new Map(this._mappings);\n\n const mapperFn = async (\n source: InferType<TFromSchema>\n ): Promise<InferType<TToSchema>> => {\n const result = {} as any;\n\n for (const [key, entry] of mappings) {\n if (entry.type === 'ignore') {\n continue;\n }\n\n let value: any;\n\n if (entry.type === 'prop') {\n const getResult =\n entry.sourceDescriptorInner.getValue(source);\n if (getResult.success) {\n value = getResult.value;\n } else {\n continue;\n }\n } else if (entry.type === 'custom') {\n value = await entry.fn!(source);\n } else if (entry.type === 'auto') {\n const getResult =\n entry.sourceDescriptorInner.getValue(source);\n if (getResult.success && getResult.value !== undefined) {\n value = await entry.autoMapper!(getResult.value);\n } else {\n continue;\n }\n } else if (entry.type === 'autoArray') {\n const getResult =\n entry.sourceDescriptorInner.getValue(source);\n // null/undefined → skip (spec §6); non-array → error\n if (getResult.success && getResult.value != null) {\n if (!Array.isArray(getResult.value)) {\n throw new MapperConfigurationError(\n `Expected array for property \"${key}\" but got ${typeof getResult.value}`\n );\n }\n value = await Promise.all(\n getResult.value.map(entry.elementMapper!)\n );\n } else {\n continue;\n }\n }\n\n const targetDescriptor = (targetTree as any)[key];\n if (targetDescriptor?.[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]) {\n targetDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ].setValue(result, value, {\n createMissingStructure: true\n });\n } else {\n result[key] = value;\n }\n }\n\n return result;\n };\n\n // Auto-register in the registry if available\n if (this._registry) {\n this._registry['_mappers']\n .get(this._fromSchema)\n ?.set(this._toSchema, mapperFn);\n }\n\n return mapperFn;\n }\n}\n\n// ── MappingRegistry ───────────────────────────────────────────────────\n\nexport class MappingRegistry<TRegistered = never> {\n protected readonly _mappers: Map<\n ObjectSchemaBuilder<any, any, any>,\n Map<\n ObjectSchemaBuilder<any, any, any>,\n SchemaToSchemaMapperResult<any, any>\n >\n > = new Map();\n\n #ensureObjectSchemas(\n fromSchema: ObjectSchemaBuilder<any, any, any>,\n toSchema: ObjectSchemaBuilder<any, any, any>\n ): boolean {\n return !(\n !fromSchema ||\n !toSchema ||\n fromSchema instanceof ObjectSchemaBuilder === false ||\n toSchema instanceof ObjectSchemaBuilder === false\n );\n }\n\n /**\n * Defines a mapping between two schemas and returns a new immutable\n * registry containing the mapping. The callback `fn` receives a fresh\n * `Mapper` and must return it after configuring property mappings.\n * The mapper is automatically finalized and registered. Properties\n * not explicitly mapped or ignored may be auto-mapped if a matching\n * nested mapping is already registered in the registry.\n *\n * @param fromSchema - source ObjectSchemaBuilder\n * @param toSchema - target ObjectSchemaBuilder\n * @param fn - callback that configures property mappings on the mapper\n * @returns a new MappingRegistry containing all previous mappings plus\n * the newly configured one\n * @throws if schemas are invalid, mapping is duplicate, or unmapped\n * properties remain that cannot be auto-mapped\n */\n public configure<\n TFromSchema extends ObjectSchemaBuilder<any, any, any>,\n TToSchema extends ObjectSchemaBuilder<any, any, any>\n >(\n fromSchema: TFromSchema,\n toSchema: TToSchema,\n fn: (\n mapper: Mapper<\n TFromSchema,\n TToSchema,\n KeysNeedingMapping<TFromSchema, TToSchema, TRegistered>,\n TRegistered\n >\n ) => Mapper<TFromSchema, TToSchema, never, TRegistered>\n ): MappingRegistry<TRegistered | [TFromSchema, TToSchema]> {\n if (!this.#ensureObjectSchemas(fromSchema, toSchema)) {\n throw new Error(\n 'Both fromSchema and toSchema must be instances of ObjectSchemaBuilder'\n );\n }\n\n // Check for duplicate mappings\n const existingFromMappers = this._mappers.get(fromSchema);\n if (existingFromMappers?.has(toSchema)) {\n throw new Error(\n 'Duplicate mapping: a mapping for this schemas pair is already registered'\n );\n }\n\n // Create new immutable registry with cloned mappings\n const newRegistry = new MappingRegistry<\n TRegistered | [TFromSchema, TToSchema]\n >();\n for (const [from, toMap] of this._mappers) {\n newRegistry._mappers.set(from, new Map(toMap));\n }\n\n // Ensure the from→to map slot exists so getMapper can register\n if (!newRegistry._mappers.has(fromSchema)) {\n newRegistry._mappers.set(fromSchema, new Map());\n }\n\n // Create mapper with new registry reference\n const mapper = new Mapper<\n TFromSchema,\n TToSchema,\n KeysNeedingMapping<TFromSchema, TToSchema, TRegistered>,\n TRegistered\n >(fromSchema, toSchema, newRegistry);\n\n // Configure via user callback\n const configuredMapper = fn(mapper);\n\n // Implicit finalization: auto-map + validate + register\n (\n configuredMapper as Mapper<\n TFromSchema,\n TToSchema,\n never,\n TRegistered\n >\n ).getMapper();\n\n return newRegistry;\n }\n\n /**\n * Gets a mapper function that will map from the fromSchema to the toSchema.\n * Throws an error if no mapper is found for the given schemas pair.\n * @param fromSchema a schema to map from\n * @param toSchema a schema to map to\n * @returns a function that will take a value of the fromSchema type as an argument and\n * return a promise resolving to a value of the toSchema type\n */\n public getMapper<\n TFromSchema extends ObjectSchemaBuilder<any, any, any>,\n TToSchema extends ObjectSchemaBuilder<any, any, any>\n >(\n fromSchema: TFromSchema,\n toSchema: TToSchema\n ): SchemaToSchemaMapperResult<TFromSchema, TToSchema> {\n if (!this.#ensureObjectSchemas(fromSchema, toSchema)) {\n throw new Error(\n 'Both fromSchema and toSchema must be instances of ObjectSchemaBuilder'\n );\n }\n\n const mappers = this._mappers.get(fromSchema);\n if (!mappers) {\n throw new Error('No mapper found for the given schemas pair');\n }\n const mapper = mappers.get(toSchema);\n if (!mapper) {\n throw new Error('No mapper found for the given schemas pair');\n }\n\n return mapper;\n }\n}\n\n/**\n * Creates a new empty {@link MappingRegistry}.\n *\n * This is a convenience factory function — an alternative to\n * `new MappingRegistry()` that reads better in a fluent chain:\n *\n * ```ts\n * const registry = mapper()\n * .configure(A, B, m => ...)\n * .configure(C, D, m => ...);\n * ```\n */\nexport function mapper(): MappingRegistry {\n return new MappingRegistry();\n}\n"],"mappings":"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","names":["ArraySchemaBuilder","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","SYMBOL_TARGET_PROPERTY_KEY","MapperConfigurationError","messageOrUnmappedProperties","resolveElementMapper","sourceElementSchema","targetElementSchema","registry","ObjectSchemaBuilder","autoMapper","fromKeys","toKeys","k","i","element","ArraySchemaBuilder","innerSourceElement","innerTargetElement","innerMapper","arr","sourceIntrospection","targetIntrospection","PropertyMappingBuilder","mapper","targetKey","selector","_args","sourceTree","inner","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","sourceSchema","targetPropSchema","elementMapper","fn","Mapper","fromSchema","toSchema","capturedKey","proxy","_target","prop","SYMBOL_TARGET_PROPERTY_KEY","toIntrospection","targetProperties","fromProperties","toProperties","fromTree","key","fromPropSchema","toPropSchema","sourceDescriptor","unmapped","targetTree","mappings","mapperFn","source","result","entry","value","getResult","targetDescriptor","MappingRegistry","_MappingRegistry","#ensureObjectSchemas","newRegistry","from","toMap","mappers"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
3
+ "bugs": {
4
+ "url": "https://github.com/cleverbrush/framework/issues",
5
+ "email": "andrew_zol@cleverbrush.com"
6
+ },
7
+ "dependencies": {
8
+ "@cleverbrush/schema": "0.0.0-beta-20260410073748"
9
+ },
10
+ "description": "Type-safe, schema-driven object mapper for @cleverbrush/schema — compile-time completeness, selector-based field mapping, auto-mapping",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "homepage": "https://docs.cleverbrush.com/modules/_cleverbrush_mapper.html",
15
+ "keywords": [
16
+ "object mapper",
17
+ "schema mapping",
18
+ "typescript",
19
+ "type-safe",
20
+ "data transformation",
21
+ "cleverbrush"
22
+ ],
23
+ "license": "BSD 3-Clause",
24
+ "main": "./dist/index.js",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ }
30
+ },
31
+ "sideEffects": false,
32
+ "name": "@cleverbrush/mapper",
33
+ "readme": "https://github.com/cleverbrush/framework/tree/master/libs/mapper#readme",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "github:cleverbrush/framework"
37
+ },
38
+ "scripts": {
39
+ "watch": "tsc --build tsconfig.build.json --watch",
40
+ "build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
41
+ "clean": "rm -rf dist tsconfig.build.tsbuildinfo"
42
+ },
43
+ "type": "module",
44
+ "types": "./dist/index.d.ts",
45
+ "version": "0.0.0-beta-20260410073748"
46
+ }