@railway-ts/pipelines 0.1.0 → 0.1.2

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 CHANGED
@@ -469,6 +469,55 @@ type Shape = InferSchemaType<typeof shapeSchema>;
469
469
  // { type: 'circle', radius: number } | { type: 'rectangle', width: number, height: number }
470
470
  ```
471
471
 
472
+ #### Tuple Validators
473
+
474
+ Validate fixed-length arrays with type-safe element validation.
475
+
476
+ ```typescript
477
+ import { tuple, tupleOf, chain, number, string, boolean, min, max, integer } from '@railway-ts/pipelines/schema';
478
+
479
+ // Heterogeneous tuple - different types per position
480
+ const userRecord = tuple([
481
+ string(), // position 0: string
482
+ chain(number(), integer(), min(0)), // position 1: non-negative integer
483
+ boolean(), // position 2: boolean
484
+ ]);
485
+
486
+ type UserRecord = InferSchemaType<typeof userRecord>;
487
+ // [string, number, boolean]
488
+
489
+ validate(['user123', 25, true], userRecord); // ok(['user123', 25, true])
490
+ validate(['user123', -5, true], userRecord); // err - age must be >= 0
491
+
492
+ // Homogeneous tuple - same type repeated N times
493
+ const vector3 = tupleOf(number(), 3); // exactly 3 numbers
494
+
495
+ type Vector3 = InferSchemaType<typeof vector3>;
496
+ // [number, number, number]
497
+
498
+ validate([1.5, 2.3, 3.7], vector3); // ok([1.5, 2.3, 3.7])
499
+ validate([1, 2], vector3); // err - expected length 3, got 2
500
+
501
+ // With constraints
502
+ const rgbColor = tupleOf(
503
+ chain(number(), integer(), min(0), max(255)),
504
+ 3
505
+ );
506
+
507
+ validate([255, 128, 64], rgbColor); // ok([255, 128, 64])
508
+ validate([255, 300, 0], rgbColor); // err - 300 exceeds max of 255
509
+
510
+ // Geographic coordinates [latitude, longitude]
511
+ const coordinate = tuple([
512
+ chain(number(), min(-90), max(90)), // latitude
513
+ chain(number(), min(-180), max(180)), // longitude
514
+ ]);
515
+
516
+ validate([37.7749, -122.4194], coordinate); // ok - San Francisco
517
+ ```
518
+
519
+ **Use cases:** Coordinates, RGB colors, version numbers, matrix rows, vectors, any fixed-length structured data.
520
+
472
521
  #### Complete Schema Example
473
522
 
474
523
  ```typescript
@@ -761,6 +810,7 @@ Working examples organized by category:
761
810
  - **Schema**: [`examples/schema/`](examples/schema/)
762
811
  - [`basic.ts`](examples/schema/basic.ts) - Basic validators
763
812
  - [`union.ts`](examples/schema/union.ts) - Union types
813
+ - [`tuple.ts`](examples/schema/tuple.ts) - Tuple validation
764
814
  - **Composition**: [`examples/composition/`](examples/composition/)
765
815
  - [`advanced-composition.ts`](examples/composition/advanced-composition.ts)
766
816
  - [`curry-basics.ts`](examples/composition/curry-basics.ts)
package/dist/index.cjs CHANGED
@@ -534,6 +534,69 @@ function stringEnum(allowedValues, message = `Value must be one of: ${allowedVal
534
534
  });
535
535
  }
536
536
 
537
+ // src/schema/tuple.ts
538
+ function tuple(validators) {
539
+ return (value, parentPath = []) => {
540
+ if (!Array.isArray(value)) {
541
+ return err([{ path: parentPath, message: "Expected an array" }]);
542
+ }
543
+ if (value.length !== validators.length) {
544
+ return err([
545
+ {
546
+ path: parentPath,
547
+ message: `Expected tuple of length ${validators.length}, got ${value.length}`
548
+ }
549
+ ]);
550
+ }
551
+ const allErrors = [];
552
+ const validatedItems = [];
553
+ for (const [i, validator] of validators.entries()) {
554
+ const itemPath = [...parentPath, i.toString()];
555
+ const item = value.at(i);
556
+ const result = validator(item, itemPath);
557
+ if (isOk(result)) {
558
+ validatedItems.push(result.value);
559
+ } else {
560
+ allErrors.push(...result.error);
561
+ }
562
+ }
563
+ if (allErrors.length > 0) {
564
+ return err(allErrors);
565
+ }
566
+ return ok(validatedItems);
567
+ };
568
+ }
569
+ function tupleOf(elementValidator, length) {
570
+ return (value, parentPath = []) => {
571
+ if (!Array.isArray(value)) {
572
+ return err([{ path: parentPath, message: "Expected an array" }]);
573
+ }
574
+ if (value.length !== length) {
575
+ return err([
576
+ {
577
+ path: parentPath,
578
+ message: `Expected tuple of length ${length}, got ${value.length}`
579
+ }
580
+ ]);
581
+ }
582
+ const allErrors = [];
583
+ const validatedItems = [];
584
+ for (const [i, item] of value.entries()) {
585
+ const itemPath = [...parentPath, i.toString()];
586
+ const result = elementValidator(item, itemPath);
587
+ if (isOk(result)) {
588
+ validatedItems.push(result.value);
589
+ } else {
590
+ allErrors.push(...result.error);
591
+ }
592
+ }
593
+ if (allErrors.length > 0) {
594
+ return err(allErrors);
595
+ }
596
+ return ok(validatedItems);
597
+ };
598
+ }
599
+
537
600
  // src/schema/number.ts
538
601
  function number(message = "Must be a number") {
539
602
  return (value, path = []) => {
@@ -1056,6 +1119,8 @@ exports.tapResult = tap;
1056
1119
  exports.toPromise = toPromise;
1057
1120
  exports.todayOrFuture = todayOrFuture;
1058
1121
  exports.transform = transform;
1122
+ exports.tuple = tuple;
1123
+ exports.tupleOf = tupleOf;
1059
1124
  exports.tupled = tupled;
1060
1125
  exports.uncurry = uncurry;
1061
1126
  exports.union = union;