11package io .github .easy4j .opencli .core ;
22
33import io .github .easy4j .opencli .util .OpenCliStrings ;
4+ import java .math .BigDecimal ;
5+ import java .math .BigInteger ;
46import java .util .ArrayList ;
57import java .util .Collections ;
8+ import java .util .HashMap ;
9+ import java .util .HashSet ;
610import java .util .LinkedHashMap ;
711import java .util .List ;
812import java .util .Map ;
913import java .util .Objects ;
14+ import java .util .Set ;
1015import lombok .AccessLevel ;
1116import lombok .Builder ;
1217import lombok .Getter ;
1318import lombok .Singular ;
1419
1520/**
16- * Structured adapter request. Positional and valued-option contents are literal;
17- * command and option identifiers are validated separately.
18- * The legacy options map represents a single-valued subset: Boolean values
19- * retain their historical presence-only flag semantics.
21+ * Structured adapter request with immutable literal values and ordered options.
22+ * The legacy options Map retains Boolean presence-only semantics; use
23+ * {@link OpenCliOption} for repeated values, valued false and explicit negation.
2024 *
2125 * @author <a href="https://github.com/loong10k">Loong Wan</a>
2226 * @since 3.0.0
2327 */
2428@ Getter
2529@ Builder
2630public final class OpenCliAdapterCommandRequest {
27-
2831 private final String subcommand ;
2932
3033 @ Getter (AccessLevel .NONE )
3134 @ Singular ("positional" )
3235 private final List <String > positionals ;
3336
3437 @ Getter (AccessLevel .NONE )
35- @ Builder .Default
36- private final Map <String , Object > options = Collections .emptyMap ();
38+ private final Map <String , Object > options ;
39+
40+ @ Getter (AccessLevel .NONE )
41+ @ Singular ("option" )
42+ private final List <OpenCliOption > orderedOptions ;
43+
44+ /**
45+ * Builder customisation snapshots legacy option values before they can be
46+ * changed through the caller's Map or a mutable value object.
47+ */
48+ public static class OpenCliAdapterCommandRequestBuilder {
49+ private Map <String , Object > options ;
50+
51+ /** @param source legacy single-value options @return this builder */
52+ public OpenCliAdapterCommandRequestBuilder options (Map <String , Object > source ) {
53+ options = snapshotOptions (source );
54+ return this ;
55+ }
56+ }
3757
3858 /** @return an immutable copy of positional values */
3959 public List <String > getPositionals () {
40- if (Objects .isNull (positionals )) {
41- return Collections .emptyList ();
42- }
43- return Collections .unmodifiableList (new ArrayList <>(positionals ));
60+ return positionals == null ? Collections .emptyList ()
61+ : Collections .unmodifiableList (new ArrayList <>(positionals ));
4462 }
4563
46- /** @return an immutable copy of named options */
64+ /** @return an immutable snapshot of legacy options */
4765 public Map <String , Object > getOptions () {
48- if (Objects .isNull (options )) {
49- return Collections .emptyMap ();
50- }
51- return Collections .unmodifiableMap (new LinkedHashMap <>(options ));
66+ return options == null ? Collections .emptyMap ()
67+ : Collections .unmodifiableMap (new LinkedHashMap <>(options ));
5268 }
5369
54- /**
55- * @return subcommand, then unchanged positional values and named options
56- */
70+ /** @return immutable, ordered option occurrences */
71+ public List <OpenCliOption > getOrderedOptions () {
72+ return orderedOptions == null ? Collections .emptyList ()
73+ : Collections .unmodifiableList (new ArrayList <>(orderedOptions ));
74+ }
75+
76+ /** @return the validated subcommand and complete literal argv */
5777 public List <String > toSubcommandAndArgs () {
5878 Objects .requireNonNull (subcommand , "subcommand" );
5979 if (OpenCliStrings .isBlank (subcommand )) {
@@ -64,27 +84,74 @@ public List<String> toSubcommandAndArgs() {
6484 if (positionals != null ) {
6585 tokens .addAll (OpenCliArgSupport .snapshotValues (positionals , "positionals" ));
6686 }
67- if (Objects .nonNull (options )) {
87+ Set <String > legacyFlags = new HashSet <>();
88+ if (options != null ) {
6889 for (Map .Entry <String , Object > entry : options .entrySet ()) {
69- appendOption (tokens , entry .getKey (), entry .getValue ());
90+ String flag = appendLegacyOption (tokens , entry .getKey (), entry .getValue ());
91+ if (flag != null && !legacyFlags .add (flag )) {
92+ throw new IllegalArgumentException ("Ambiguous duplicate legacy option identifier" );
93+ }
94+ }
95+ }
96+ Map <String , OpenCliOptionSchema > seen = new HashMap <>();
97+ Map <String , String > wireNames = new HashMap <>();
98+ if (orderedOptions != null ) {
99+ for (int i = 0 ; i < orderedOptions .size (); i ++) {
100+ OpenCliOption occurrence = orderedOptions .get (i );
101+ if (occurrence == null ) {
102+ throw new IllegalArgumentException ("orderedOptions[" + i + "] must not be null" );
103+ }
104+ OpenCliOptionSchema schema = occurrence .getSchema ();
105+ String name = schema .getName ();
106+ List <String > argv = occurrence .toTokens ();
107+ String wireName = argv .get (0 );
108+ if (legacyFlags .contains (name ) || legacyFlags .contains (wireName )) {
109+ throw new IllegalArgumentException ("Legacy and ordered options must not overlap" );
110+ }
111+ OpenCliOptionSchema previous = seen .put (name , schema );
112+ if (previous != null && (!schema .isRepeatable () || !previous .equals (schema ))) {
113+ throw new IllegalArgumentException ("Repeated option is not allowed by one consistent schema" );
114+ }
115+ String previousCanonical = wireNames .put (wireName , name );
116+ if (previousCanonical != null && !previousCanonical .equals (name )) {
117+ throw new IllegalArgumentException ("Different schemas emit the same option identifier" );
118+ }
119+ tokens .addAll (argv );
70120 }
71121 }
72122 return tokens ;
73123 }
74124
75- private static void appendOption (List <String > target , String name , Object value ) {
76- if (OpenCliStrings .isBlank (name ) || Objects .isNull (value )) {
77- return ;
78- }
79- String flag = name .startsWith ("-" ) ? name .trim () : "--" + name .trim ();
80- if (value instanceof Boolean ) {
81- if (((Boolean ) value ).booleanValue ()) {
82- target .add (flag );
125+ private static Map <String , Object > snapshotOptions (Map <String , Object > source ) {
126+ if (source == null ) { return Collections .emptyMap (); }
127+ Map <String , Object > snapshot = new LinkedHashMap <>();
128+ for (Map .Entry <String , Object > entry : new LinkedHashMap <>(source ).entrySet ()) {
129+ Object value = entry .getValue ();
130+ if (value != null ) {
131+ Class <?> type = value .getClass ();
132+ if (type != String .class && type != Boolean .class && type != Byte .class
133+ && type != Short .class && type != Integer .class && type != Long .class
134+ && type != Float .class && type != Double .class && type != BigInteger .class
135+ && type != BigDecimal .class ) {
136+ value = String .valueOf (value );
137+ }
83138 }
84- return ;
139+ snapshot . put ( entry . getKey (), value ) ;
85140 }
141+ return Collections .unmodifiableMap (snapshot );
142+ }
143+
144+ private static String appendLegacyOption (List <String > target , String name , Object value ) {
145+ if (OpenCliStrings .isBlank (name ) || value == null || Boolean .FALSE .equals (value )) {
146+ return null ;
147+ }
148+ String normalized = name .trim ();
149+ String flag = normalized .startsWith ("-" ) ? normalized : "--" + normalized ;
86150 target .add (flag );
87- target .add (String .valueOf (value ));
151+ if (!(value instanceof Boolean )) {
152+ target .add (String .valueOf (value ));
153+ }
154+ return flag ;
88155 }
89156
90157 /**
@@ -94,16 +161,10 @@ private static void appendOption(List<String> target, String name, Object value)
94161 * @return a structured request
95162 */
96163 public static OpenCliAdapterCommandRequest of (
97- String subcommand ,
98- List <String > positionals ,
99- Map <String , Object > options ) {
164+ String subcommand , List <String > positionals , Map <String , Object > options ) {
100165 OpenCliAdapterCommandRequestBuilder b = builder ().subcommand (subcommand );
101- if (Objects .nonNull (positionals )) {
102- b .positionals (positionals );
103- }
104- if (Objects .nonNull (options ) && !options .isEmpty ()) {
105- b .options (new LinkedHashMap <>(options ));
106- }
166+ if (positionals != null ) { b .positionals (positionals ); }
167+ if (options != null ) { b .options (options ); }
107168 return b .build ();
108169 }
109170}
0 commit comments