@@ -8,11 +8,12 @@ import {
88 getLocale ,
99 parseArgs ,
1010 parseArgsWithCliOptions ,
11- UnknownCliArgumentError ,
12- UnsupportedCliArgumentError ,
11+ UnknownArgumentError ,
12+ UnsupportedArgumentError ,
1313} from './arg-parser' ;
1414import { z } from 'zod/v4' ;
1515import { coerceIfBoolean , coerceIfFalse } from './utils' ;
16+ import { InvalidArgumentError } from './arg-metadata' ;
1617
1718describe ( 'arg-parser' , function ( ) {
1819 describe ( '.getLocale' , function ( ) {
@@ -300,18 +301,9 @@ describe('arg-parser', function () {
300301 const argv = [ uri , '--what' ] ;
301302
302303 it ( 'raises an error' , function ( ) {
303- try {
304+ expect ( ( ) => {
304305 parseArgsWithCliOptions ( { args : argv } ) . parsed ;
305- } catch ( err : any ) {
306- if ( err instanceof UnknownCliArgumentError ) {
307- expect ( stripAnsi ( err . message ) ) . to . equal (
308- 'Unknown argument: --what'
309- ) ;
310- return ;
311- }
312- expect . fail ( 'Expected UnknownCliArgumentError' ) ;
313- }
314- expect . fail ( 'parsing unknown parameter did not throw' ) ;
306+ } ) . to . throw ( UnknownArgumentError , 'Unknown argument: --what' ) ;
315307 } ) ;
316308 } ) ;
317309 } ) ;
@@ -438,7 +430,7 @@ describe('arg-parser', function () {
438430 expect (
439431 ( ) => parseArgsWithCliOptions ( { args : argv } ) . parsed
440432 ) . to . throw (
441- UnsupportedCliArgumentError ,
433+ UnsupportedArgumentError ,
442434 'Unsupported argument: gssapiHostName'
443435 ) ;
444436 } ) ;
@@ -655,7 +647,7 @@ describe('arg-parser', function () {
655647 expect (
656648 ( ) => parseArgsWithCliOptions ( { args : argv } ) . parsed
657649 ) . to . throw (
658- UnsupportedCliArgumentError ,
650+ UnsupportedArgumentError ,
659651 'Unsupported argument: sslFIPSMode'
660652 ) ;
661653 } ) ;
@@ -1417,6 +1409,155 @@ describe('arg-parser', function () {
14171409 } ,
14181410 } ) ;
14191411 } ) ;
1412+
1413+ describe ( 'object fields' , function ( ) {
1414+ it ( 'parses object fields' , function ( ) {
1415+ const options = parseArgs ( {
1416+ args : [ '--objectField' , '{"foo":"bar"}' ] ,
1417+ schema : z . object ( {
1418+ objectField : z . object ( {
1419+ foo : z . string ( ) ,
1420+ } ) ,
1421+ } ) ,
1422+ } ) ;
1423+
1424+ expect ( options . parsed ) . to . deep . equal ( {
1425+ objectField : {
1426+ foo : 'bar' ,
1427+ } ,
1428+ } ) ;
1429+ } ) ;
1430+
1431+ it ( 'enforces the schema of the object field' , function ( ) {
1432+ const schema = z . object ( {
1433+ objectField : z . object ( {
1434+ foo : z . number ( ) ,
1435+ } ) ,
1436+ } ) ;
1437+ expect (
1438+ parseArgs ( {
1439+ args : [ '--objectField' , '{"foo":3}' ] ,
1440+ schema,
1441+ } ) . parsed . objectField
1442+ ) . to . deep . equal ( { foo : 3 } ) ;
1443+ expect ( ( ) =>
1444+ parseArgs ( {
1445+ args : [ '--objectField' , '{"foo":"hello"}' ] ,
1446+ schema,
1447+ } )
1448+ ) . to . throw ( InvalidArgumentError , 'expected number, received string' ) ;
1449+ } ) ;
1450+
1451+ it ( 'can handle --a.b format' , ( ) => {
1452+ const schema = z . object ( {
1453+ a : z . object ( {
1454+ number : z . number ( ) ,
1455+ string : z . string ( ) ,
1456+ boolean : z . boolean ( ) ,
1457+ } ) ,
1458+ } ) ;
1459+ expect (
1460+ parseArgs ( {
1461+ args : [
1462+ '--a.number' ,
1463+ '3' ,
1464+ '--a.string' ,
1465+ 'hello' ,
1466+ '--a.boolean' ,
1467+ 'true' ,
1468+ ] ,
1469+ schema,
1470+ } ) . parsed . a
1471+ ) . to . deep . equal ( {
1472+ number : 3 ,
1473+ string : 'hello' ,
1474+ boolean : true ,
1475+ } ) ;
1476+ } ) ;
1477+
1478+ it ( 'can handle nested object fields' , ( ) => {
1479+ const schema = z . object ( {
1480+ parent : z . object ( {
1481+ child : z . string ( ) ,
1482+ nested : z . object ( {
1483+ deep : z . number ( ) ,
1484+ } ) ,
1485+ } ) ,
1486+ } ) ;
1487+ expect (
1488+ parseArgs ( {
1489+ args : [ '--parent.child' , 'hello' , '--parent.nested.deep' , '42' ] ,
1490+ schema,
1491+ } ) . parsed . parent
1492+ ) . to . deep . equal ( {
1493+ child : 'hello' ,
1494+ nested : {
1495+ deep : 42 ,
1496+ } ,
1497+ } ) ;
1498+ } ) ;
1499+
1500+ it ( 'can handle multiple types in nested objects' , ( ) => {
1501+ const schema = z . object ( {
1502+ config : z . object ( {
1503+ enabled : z . boolean ( ) ,
1504+ name : z . string ( ) ,
1505+ count : z . number ( ) ,
1506+ tags : z . array ( z . string ( ) ) ,
1507+ } ) ,
1508+ } ) ;
1509+ const result = parseArgs ( {
1510+ args : [
1511+ '--config.enabled' ,
1512+ '--config.name' ,
1513+ 'test' ,
1514+ '--config.count' ,
1515+ '10' ,
1516+ '--config.tags' ,
1517+ 'tag1' ,
1518+ '--config.tags' ,
1519+ 'tag2' ,
1520+ ] ,
1521+ schema,
1522+ } ) ;
1523+ expect ( result . parsed . config ) . to . deep . equal ( {
1524+ enabled : true ,
1525+ name : 'test' ,
1526+ count : 10 ,
1527+ tags : [ 'tag1' , 'tag2' ] ,
1528+ } ) ;
1529+ } ) ;
1530+
1531+ it ( 'generateYargsOptionsFromSchema processes nested objects' , ( ) => {
1532+ const schema = z . object ( {
1533+ server : z . object ( {
1534+ host : z . string ( ) ,
1535+ port : z . number ( ) ,
1536+ ssl : z . boolean ( ) ,
1537+ } ) ,
1538+ } ) ;
1539+ const options = generateYargsOptionsFromSchema ( { schema } ) ;
1540+
1541+ expect ( options . string ) . to . include ( 'server.host' ) ;
1542+ expect ( options . number ) . to . include ( 'server.port' ) ;
1543+ expect ( options . boolean ) . to . include ( 'server.ssl' ) ;
1544+ expect ( options . coerce ) . to . have . property ( 'server' ) ;
1545+ } ) ;
1546+
1547+ it ( 'generateYargsOptionsFromSchema processes deeply nested objects' , ( ) => {
1548+ const schema = z . object ( {
1549+ level1 : z . object ( {
1550+ level2 : z . object ( {
1551+ level3 : z . string ( ) ,
1552+ } ) ,
1553+ } ) ,
1554+ } ) ;
1555+ const options = generateYargsOptionsFromSchema ( { schema } ) ;
1556+
1557+ expect ( options . string ) . to . include ( 'level1.level2.level3' ) ;
1558+ expect ( options . coerce ) . to . have . property ( 'level1' ) ;
1559+ } ) ;
1560+ } ) ;
14201561 } ) ;
14211562
14221563 describe ( 'parseArgsWithCliOptions' , function ( ) {
@@ -1450,13 +1591,23 @@ describe('arg-parser', function () {
14501591 'true' ,
14511592 '--deprecatedField' ,
14521593 '100' ,
1594+ '--complexField' ,
1595+ 'false' ,
14531596 ] ,
14541597 schema : z . object ( {
14551598 extendedField : z . number ( ) ,
14561599 replacedField : z . number ( ) ,
14571600 deprecatedField : z . number ( ) . register ( argMetadata , {
14581601 deprecationReplacement : 'replacedField' ,
14591602 } ) ,
1603+ // TODO: The expected behavior right now is pre-processing doesn't happen as part of the arg-parser.
1604+ // What we instead focus on is making sure the output is passed as expected type (i.e. z.boolean())
1605+ // The assumption is that external users will pass the output through their schema after this parse.
1606+ // With greater testing, we should support schema assertion directly in the parser.
1607+ complexField : z . preprocess (
1608+ ( value : unknown ) => value === 'true' ,
1609+ z . boolean ( )
1610+ ) ,
14601611 } ) ,
14611612 } ) ;
14621613
@@ -1468,6 +1619,7 @@ describe('arg-parser', function () {
14681619 extendedField : 90 ,
14691620 tls : true ,
14701621 fileNames : [ ] ,
1622+ complexField : false ,
14711623 } ,
14721624 deprecated : {
14731625 ssl : 'tls' ,
@@ -1491,7 +1643,7 @@ describe('arg-parser', function () {
14911643 extendedField : z . enum ( [ '90' , '100' ] ) ,
14921644 } ) ,
14931645 } )
1494- ) . to . throw ( UnknownCliArgumentError , 'Unknown argument: --unknownField' ) ;
1646+ ) . to . throw ( UnknownArgumentError , 'Unknown argument: --unknownField' ) ;
14951647 } ) ;
14961648 } ) ;
14971649} ) ;
0 commit comments