001/************************* PROJECT RON *************************/ 002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved. */ 003/* Use of this source code is governed by an MIT-style license */ 004/* that can be found in the repository LICENSE file. */ 005/***************************************************************/ 006package tools.ToolClasses; 007 008import java.util.Arrays; 009import java.util.HashMap; 010import java.util.Map; 011 012/** 013 * <h2>ArgumentedTool</h2> 014 * A class that represents a tool that takes in arguments. 015 * <p>Using gradle tasks with arguments, it automatically parses the arguments based on an enum that the user defines and passes in. 016 * <p>The enum must implement the {@link ArgumentEnum} interface, which must specify a type via {@link ArgumentEnum#getArgumentType()}, 017 * default value (can be null) via {@link ArgumentEnum#getDefaultValue()}, and description for each argument via {@link ArgumentEnum#getDescription()}. 018 * The order of the arguments is determined by the order of the enum constants. 019 * 020 * <p>When parsing, it does the following: 021 * <ol> 022 * <li>Splits the raw arguments by spaces, unless they are within quotes. (e.g. <code>-Pargs="arg1 'arg 2' arg3"</code> => <code>[arg1, 'arg 2', arg3]</code>)</li> 023 * <li>Removes the surrounding quotes from the split arguments (e.g. <code>[arg1, 'arg 2', arg3]</code> => <code>[arg1, arg 2, arg3]</code>)</li> 024 * <li>Iterates through the split arguments and attempts to parse them into the type specified by the enum constant {@link ArgumentEnum#getArgumentType()}. 025 * <ul> 026 * <li>If the type is a string, then it will stay as is.</li> 027 * <li>If the type is an integer, then it will attempt to parse it into an integer.</li> 028 * <li>If the type is a double, then it will attempt to parse it into a double.</li> 029 * <li>If the type is a boolean, then it will attempt to parse it into a boolean.</li> 030 * <li>If the type is an enum, then it will attempt to parse it into the enum by matching the name of the enum constant (case-insensitive).</li> 031 * </ul> 032 * </li> 033 * <li>If the argument is missing or fails to parse, then it will use the default value specified by the enum. 034 * If there is no default value and the argument fails to parse, then it will throw an exception.</li> 035 * <li>Extra arguments that are specified, greater than the length of the enum constant, will be ignored.</li> 036 * </ol> 037 * <p>The constructor takes in an enum class that defines the arguments. To use the, the {@link #execute(Map)} method must be implemented. 038 * This method has the parsed arguments passed in as a map, where the keys are the from the argument enum constants, and the values are the parsed arguments 039 * from the raw string passed into the {@link #run(String)} method. 040 * 041 * <p>The {@link #run(String)} method is the main entry point for the tool. This will automatically parse the raw string arguments and call the 042 * {@link #execute(Map)} method with the parsed arguments. 043 */ 044public abstract class ArgumentedTool<E extends Enum<E> & ArgumentEnum> { 045 private final Class<E> argumentsEnumClass; 046 047 @SuppressWarnings("unchecked") 048 private Map<E, Object> parseArguments(String rawArgs) { 049 String[] splitArgs = rawArgs.split(" (?=(?:[^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); // split by spaces if not within single or double quotes 050 // all regex statements in this repo are vibe coded 051 Object[] argumentValues = new Object[splitArgs.length]; 052 for (int i = 0; i < splitArgs.length; i++) { 053 argumentValues[i] = splitArgs[i].replaceAll("^\"|\"$|^'|'$", ""); // remove surrounding quotes 054 055 E currentEnum = argumentsEnumClass.getEnumConstants()[i]; 056 if (currentEnum.getArgumentType() == String.class) continue; 057 058 switch (argumentsEnumClass.getEnumConstants()[i].getArgumentType().getSimpleName()) { 059 case "Integer", "int" -> { 060 try { 061 argumentValues[i] = Integer.parseInt((String) argumentValues[i]); 062 } catch (Exception e) { 063 if (currentEnum.getDefaultValue() == null || currentEnum.getDefaultValue().getClass() != Integer.class) { 064 throw new IllegalArgumentException("Argument " + currentEnum.name() + "(" + currentEnum.getDescription() + ") must be an integer"); 065 } 066 } 067 break; 068 } 069 case "Double", "double" -> { 070 try { 071 argumentValues[i] = Double.parseDouble((String) argumentValues[i]); 072 } catch (Exception e) { 073 if (currentEnum.getDefaultValue() == null || currentEnum.getDefaultValue().getClass() != Double.class) { 074 throw new IllegalArgumentException("Argument #" + (i + 1) + " (" + currentEnum.getDescription() + ") must be a double"); 075 } 076 } 077 break; 078 } 079 case "Boolean", "boolean" -> { 080 if (((String) argumentValues[i]).equalsIgnoreCase("true") || ((String) argumentValues[i]).equalsIgnoreCase("false")) { 081 argumentValues[i] = Boolean.parseBoolean((String) argumentValues[i]); 082 } else { 083 if (currentEnum.getDefaultValue() == null || currentEnum.getDefaultValue().getClass() != Boolean.class) { 084 throw new IllegalArgumentException("Argument #" + (i + 1) + " (" + currentEnum.getDescription() + ") must be a boolean"); 085 } 086 } 087 break; 088 } 089 default -> { 090 if (currentEnum.getArgumentType().isEnum()) { 091 Enum<E>[] enumConstants = (Enum<E>[]) currentEnum.getArgumentType().getEnumConstants(); 092 try { 093 String enumName = (String) argumentValues[i]; 094 Enum<?> match = null; 095 for (Enum<?> c : (Enum<?>[]) currentEnum.getArgumentType().getEnumConstants()) { 096 if (c.name().equalsIgnoreCase(enumName)) { 097 match = c; 098 break; 099 } 100 } 101 102 if (match == null) { 103 throw new IllegalArgumentException(); // to be caught by catch block 104 } 105 argumentValues[i] = match; 106 } catch (Exception e) { 107 throw new IllegalArgumentException("Argument #" + (i + 1) + " (" + currentEnum.getDescription() + ") must be one of " + Arrays.toString(enumConstants)); 108 } 109 } else { 110 argumentValues[i] = currentEnum.getDefaultValue(); 111 } 112 } 113 } 114 } 115 116 Map<E, Object> parsedArgs = new HashMap<>(); 117 for (int i = 0; i < argumentsEnumClass.getEnumConstants().length; i++) { 118 E currentEnum = argumentsEnumClass.getEnumConstants()[i]; 119 if (i < argumentValues.length) { 120 parsedArgs.put(currentEnum, argumentValues[i]); 121 } else { 122 parsedArgs.put(currentEnum, currentEnum.getDefaultValue()); 123 } 124 } 125 126 System.out.println(parsedArgs + " - Parsed Args"); 127 return parsedArgs; 128 } 129 130 protected ArgumentedTool(Class<E> argumentsEnumClass) { 131 this.argumentsEnumClass = argumentsEnumClass; 132 } 133 134 /** 135 * The functionality for the tool. Must be implemented. 136 * @param parsedArgs the parsed arguments 137 */ 138 protected abstract void execute(Map<E, Object> parsedArgs); 139 /** 140 * The main entry point for the tool. This will call the {@link #execute(Map)} method with the parsed arguments. 141 */ 142 public final void run(String rawArgs) { 143 Map<E, Object> parsedArgs = parseArguments(rawArgs); 144 execute(parsedArgs); 145 } 146}