1 module jcli.core.flags;
2 
3 enum ArgConfig : ArgFlags
4 {
5     ///
6     none,
7 
8     /// If the argument appears multiple times, the last value provided will take effect.
9     canRedefine = ArgFlags._canRedefineBit | ArgFlags._multipleBit | ArgFlags._namedArgumentBit,
10 
11     /// If not given a value, will have its default value and not trigger an error.
12     /// Missing (even named) arguments trigger an error by default.
13     optional = ArgFlags._optionalBit,
14 
15     /// The opposite of optional.
16     required = ArgFlags._requiredBit,
17 
18     /// The name of the argument is case insensitive.
19     /// Aka "--STUFF" will work in place of "--stuff".
20     caseInsensitive = ArgFlags._caseInsensitiveBit,
21 
22     /// Example: `-a -a` gives 2.
23     accumulate = ArgFlags._multipleBit | ArgFlags._countBit | ArgFlags._namedArgumentBit,
24 
25     /// The type of the field must be an array of some sort.
26     /// Example: `-a b -a c` gives an the array ["b", "c"]
27     aggregate = ArgFlags._multipleBit | ArgFlags._aggregateBit | ArgFlags._namedArgumentBit,
28 
29     /// When an argument name is specified multiple times, count how many there are.
30     /// Example: `-vvv` gives the count of 3.
31     repeatableName = ArgFlags._repeatableNameBit | ArgFlags._countBit | ArgFlags._namedArgumentBit,
32 
33     /// Allow an argument name to appear without a value.
34     /// Example: `--flag` would parse as `true`.
35     parseAsFlag = ArgFlags._parseAsFlagBit | ArgFlags._optionalBit | ArgFlags._namedArgumentBit,
36 
37     /// Can be used instead of ArgPositional UDA.
38     positional = ArgFlags._positionalArgumentBit,
39 
40     /// Can be used instead of ArgNamed UDA.
41     named = ArgFlags._namedArgumentBit,
42 }
43 unittest
44 {
45     static foreach (name; __traits(allMembers, ArgConfig))
46     {{
47         ArgConfig flags = __traits(getMember, ArgConfig, name);
48         string validation = getArgumentFlagsValidationMessage(flags);
49         assert(validation is null);
50     }}
51 }
52 
53 private
54 {
55     /// Shows which other flags a feature is incompatible with.
56     /// The flags should only declare this regarding the flags after them. 
57     struct IncompatibleWithAnyOf
58     {
59         ArgFlags value;
60     }
61 
62     /// Shows which other flags a feature is requred to be accompanied with.
63     struct RequiresOneOrMoreOf
64     {
65         ArgFlags value;
66     }
67 
68     /// Shows which flags a feature requres exactly one of.
69     struct RequiresExactlyOneOf
70     {
71         ArgFlags value;
72     }
73 
74     /// Shows which other flags a feature is requres with.
75     struct RequiresAllOf
76     {
77         ArgFlags value;
78     }
79 }
80 
81 package(jcli):
82 
83 enum ArgFlags
84 {
85     ///
86     none,
87 
88     /// If not given a value, will have its default value and not trigger an error.
89     _optionalBit = 1 << 0,
90 
91     /// An argument with the same name may appear multiple times.
92     @RequiresExactlyOneOf(_countBit | _canRedefineBit | _aggregateBit)
93     _multipleBit = 1 << 1,
94 
95     /// Allow an argument name to appear without a value.
96     @IncompatibleWithAnyOf(_multipleBit)
97     @RequiresAllOf(_optionalBit)
98     _parseAsFlagBit = 1 << 2,
99 
100     /// Implies that the field should get the number of occurences of the argument.
101     @RequiresOneOrMoreOf(_multipleBit | _repeatableNameBit)
102     _countBit = 1 << 3,
103 
104     /// The name of the argument is case insensitive.
105     /// Aka "--STUFF" will work in place of "--stuff".
106     _caseInsensitiveBit = 1 << 4,
107 
108     /// If the argument appears multiple times, the last value provided will take effect.
109     @IncompatibleWithAnyOf(_countBit)
110     @RequiresAllOf(_multipleBit)
111     _canRedefineBit = 1 << 5,
112 
113     /// When an argument name is specified multiple times, count how many there are.
114     /// Example: `-vvv` gives the count of 3.
115     @IncompatibleWithAnyOf(_parseAsFlagBit | _canRedefineBit)
116     @RequiresAllOf(_countBit)
117     _repeatableNameBit = 1 << 6,
118 
119     /// Put all matched values in an array. 
120     @IncompatibleWithAnyOf(_parseAsFlagBit | _countBit | _canRedefineBit)
121     _aggregateBit = 1 << 7,
122 
123     /// The opposite of optional. Can usually be inferred.
124     @IncompatibleWithAnyOf(_optionalBit | _parseAsFlagBit)
125     _requiredBit = 1 << 8,
126 
127     /// Whether the required bit or optional bit was given explicitly by the user
128     /// or inferred by the system.
129     _inferedOptionalityBit = 1 << 9,
130 
131     /// Meets the requirements of having their optionality being changed.
132     /// This bit is kind of pointless so I will probably remove it.
133     @RequiresAllOf(_inferedOptionalityBit)
134     _mayChangeOptionalityWithoutBreakingThingsBit = 1 << 10,
135 
136     /// Whether is positional. 
137     /// On the user side, it is usually provided via UDAs.
138     @IncompatibleWithAnyOf(
139         _multipleBit
140         | _parseAsFlagBit
141         | _countBit
142         | _canRedefineBit
143         | _repeatableNameBit
144         | _aggregateBit)
145     _positionalArgumentBit = 1 << 11,
146 
147     /// Whether is positional. 
148     /// On the user side, it is usually provided via UDAs.
149     @IncompatibleWithAnyOf(_positionalArgumentBit)
150     _namedArgumentBit = 1 << 12,
151 
152     /// Whether a field is of struct type, containing subfields, which are also arguments.
153     /// This means all fields of that nested struct are to be considered arguments, if marked as such.
154     /// I could force the user to 
155     // _nestedStructBit = 1 << 13,
156 }
157 
158 bool areArgumentFlagsValid(ArgFlags flags) pure @safe
159 {
160     // for not just call into get message, but this can be optimized a little bit later.
161     return getArgumentFlagsValidationMessage(flags) is null;
162 }
163 
164 // string toFlagsString(ArgFlags flags) pure @safe
165 // {
166 //     return jcli.core.utils.toFlagsString(flags);
167 // }
168 
169 alias ArgFlagsInfo = _ArgFlagsInfo!(ArgFlags);
170 
171 import jcli.core.utils : toFlagsString;
172 import std.bitmanip : bitsSet;        
173 import std.conv : to;
174 
175 string getArgumentFlagsValidationMessage(ArgFlags flags) pure @safe
176 {
177     foreach (size_t index; bitsSet(flags))
178     {
179         const currentFlag = cast(ArgFlags)(1 << index);
180         {
181             const f = ArgFlagsInfo.incompatible[index];
182             if (flags.hasEither(f))
183             {
184                 return "The flag " ~ currentFlag.toFlagsString ~ " is incompatible with " ~ f.toFlagsString;
185             }
186         }
187         {
188             const f = ArgFlagsInfo.requiresAll[index];
189             if (flags.doesNotHave(f))
190             {
191                 return "The flag " ~ currentFlag.toFlagsString ~ " requires all of " ~ f.toFlagsString;
192             }
193         }
194         {
195             const f = ArgFlagsInfo.requiresOneOrMore[index];
196             if (f != 0 && flags.doesNotHaveEither(f))
197             {
198                 return "The flag " ~ currentFlag.toFlagsString ~ " requires one or more of " ~ f.toFlagsString;
199             }
200         }
201         {
202             const f = ArgFlagsInfo.requiresExactlyOne[index];
203             if (f != 0)
204             {
205                 auto matches = flags & f;
206 
207                 import std.range : walkLength;
208                 auto numMatches = bitsSet(matches).walkLength;
209 
210                 if (numMatches != 1)
211                 {
212                     return "The flag " ~ currentFlag.toFlagsString ~ " requires exactly one of " ~ f.toFlagsString
213                         ~ ". Were matched all of the following: " ~ matches.toFlagsString;
214                 }
215             }
216         }
217     }
218 
219     return null;
220 }
221 
222 /// Returns the validation message that describes in what way
223 /// any pairs of flags were incompatible.
224 /// We only do compile-time assertions, which is why this function 
225 /// just concatenates strings to return the error message (basically, it shouldn't matter).
226 string getArgumentConfigFlagsIncompatibilityValidationMessage(ArgConfig[] flagsToTest)
227 {
228     foreach (index, f1; flagsToTest)
229     {
230         foreach (f2; flagsToTest[index + 1 .. $])
231         {
232             if (f1 == f2)
233                 continue;
234 
235             foreach (bitIndex; bitsSet(f1))
236             {
237                 auto incompatibleForThisBit = ArgFlagsInfo.incompatible[bitIndex];
238 
239                 if (incompatibleForThisBit & f1)
240                 {
241                     return "The high-level config flag " ~ f1.to!string
242                         ~ " is invalid, the low level parts are incompatible. The bit "
243                         ~ (cast(ArgFlags)(1 << bitIndex)).to!string
244                         ~ " is incompatible with " ~ incompatibleForThisBit.toFlagsString;
245                 }
246 
247                 auto problematicFlags = cast(ArgFlags) (incompatibleForThisBit & f2);
248                 if (problematicFlags)
249                 {
250                     return "The high-level config flag " ~ f1.to!string
251                         ~ " is incompatible with " ~ f2.to!string
252                         ~ ". The problematic flags are " ~ problematicFlags.toFlagsString;
253                 }
254             }
255         }
256     }
257     return null;
258 }
259 
260 enum CommandFlags
261 {
262     none = 0,
263 
264     /// No command attribute was found.
265     noCommandAttribute = 1 << 0,
266 
267     /// The command attribute was deduced from its simple form, aka @("description").
268     stringAttribute = 1 << 1,
269 
270     /// The command was set from a Command or CommandDefault attribute
271     commandAttribute = 1 << 2,
272 
273     /// e.g. @Command("ab", "cd") instead of @Command.
274     givenValue = 1 << 3,
275 
276     ///
277     explicitlyDefault = 1 << 4,
278 }
279 
280 import jcli.core.utils : FlagsHelpers;
281 mixin FlagsHelpers!ArgFlags;
282 mixin FlagsHelpers!CommandFlags;
283 
284 @safe nothrow @nogc pure const
285 unittest
286 {
287     with (ArgFlags)
288     {
289         ArgFlags a = _aggregateBit | _caseInsensitiveBit;
290         ArgFlags b = _canRedefineBit;
291         assert(doesNotHave(a, b));  
292         assert(doesNotHave(a, b | _caseInsensitiveBit));  
293         assert(has(a, _caseInsensitiveBit));  
294         assert(hasEither(a, b | _caseInsensitiveBit));
295         assert(hasEither(a, _caseInsensitiveBit)); 
296         assert(doesNotHaveEither(a, b));
297     }
298 }
299 
300 
301 
302 private template _ArgFlagsInfo(Flags)
303 {
304     immutable Flags[argFlagCount] incompatible        = getFlagsInfo!IncompatibleWithAnyOf;
305     immutable Flags[argFlagCount] requiresAll         = getFlagsInfo!RequiresAllOf;
306     immutable Flags[argFlagCount] requiresExactlyOne  = getFlagsInfo!RequiresExactlyOneOf;
307     immutable Flags[argFlagCount] requiresOneOrMore   = getFlagsInfo!RequiresOneOrMoreOf;
308 
309     enum argFlagCount = 
310     (){
311         import std.algorithm;
312         import std.range;
313         return Flags.max.bitsSet.array[$ - 1] + 1;
314     }();
315 
316     Flags[argFlagCount] getFlagsInfo(TUDA)()
317     {
318         Flags[argFlagCount] result;
319 
320         static foreach (memberName; __traits(allMembers, Flags))
321         {{
322             size_t index = __traits(getMember, Flags, memberName).bitsSet.front;
323             static foreach (uda; __traits(getAttributes, __traits(getMember, Flags, memberName)))
324             {
325                 static if (is(typeof(uda) == TUDA))
326                 {
327                     result[index] = uda.value;
328                 }
329             }
330         }}
331 
332         return result;
333     }
334 }