From e3447c08c257ee99e0ab1723e094d411f4a4bc87 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:36:08 +0100 Subject: [PATCH 1/4] fix(generator): preserve choice branches around resource groups --- ...gcloud-apihub-plugins-instances-create.txt | 367 ++++++++++++ ...loud-oracle-database-db-systems-create.txt | 540 ++++++++++++++++++ .../GcloudPeerResourceValidationTests.cs | 71 +++ .../Scrapers/UsageSynopsisParserTests.cs | 22 + .../Scrapers/Cli/CliArgumentGroupParser.cs | 11 +- .../Scrapers/Cli/UsageSynopsisParser.cs | 10 +- 6 files changed, 1015 insertions(+), 6 deletions(-) create mode 100644 tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-apihub-plugins-instances-create.txt create mode 100644 tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-oracle-database-db-systems-create.txt create mode 100644 tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GcloudPeerResourceValidationTests.cs diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-apihub-plugins-instances-create.txt b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-apihub-plugins-instances-create.txt new file mode 100644 index 00000000000..348f5eb8e20 --- /dev/null +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-apihub-plugins-instances-create.txt @@ -0,0 +1,367 @@ +NAME + gcloud apihub plugins instances create - create a Plugin Instance + +SYNOPSIS + gcloud apihub plugins instances create + (INSTANCE : --location=LOCATION --plugin=PLUGIN) + --actions=[actionId=ACTIONID],[curationConfig=CURATIONCONFIG], + [scheduleCronExpression=SCHEDULECRONEXPRESSION], + [scheduleTimeZone=SCHEDULETIMEZONE],[serviceAccount=SERVICEACCOUNT] + --display-name=DISPLAY_NAME + [--additional-config=[ADDITIONAL_CONFIG,...]] [--async] + [--source-environments-config=[SOURCE_ENVIRONMENTS_CONFIG,...]] + [--source-project-id=SOURCE_PROJECT_ID] + [--auth-config-type=AUTH_CONFIG_TYPE + : --api-key-config-http-element-location=API_KEY_CONFIG_HTTP_ELEMENT_LOCATION --api-key-config-name=API_KEY_CONFIG_NAME --api-key-config-secret-version=API_KEY_CONFIG_SECRET_VERSION | --oauth2-client-credentials-config-id=OAUTH2_CLIENT_CREDENTIALS_CONFIG_ID --oauth2-client-credentials-config-secret-version=OAUTH2_CLIENT_CREDENTIALS_CONFIG_SECRET_VERSION | --user-password-config-secret-version=USER_PASSWORD_CONFIG_SECRET_VERSION --user-password-config-username=USER_PASSWORD_CONFIG_USERNAME] + [GCLOUD_WIDE_FLAG ...] + +DESCRIPTION + Create a plugin instance + + Note: The positional argument for Plugin Instance ID is currently not + supported. Please use the --plugin-instance flag to specify the Plugin + Instance ID. + +EXAMPLES + To create a plugin instance with the ID my-instance for plugin my-plugin, + run: + + $ gcloud apihub plugins instances create \ + --plugin-instance=my-instance --plugin=my-plugin \ + --display-name="My Instance" --project=my-project \ + --location=us-central1 + +POSITIONAL ARGUMENTS + Instance resource - Identifier. The unique name of the plugin instance + resource. Format: + projects/{project}/locations/{location}/plugins/{plugin}/instances/{instance} + The arguments in this group can be used to specify the attributes of this + resource. (NOTE) Some attributes are not given arguments in this group but + can be set in other ways. + + To set the project attribute: + * provide the argument instance on the command line with a fully + specified name; + * provide the argument --project on the command line; + * set the property core/project. + + This must be specified. + + INSTANCE + ID of the instance or fully qualified identifier for the instance. + + To set the instance attribute: + + provide the argument instance on the command line. + + This positional argument must be specified if any of the other + arguments in this group are specified. + + --location=LOCATION + The location id of the instance resource. + + To set the location attribute: + + provide the argument instance on the command line with a fully + specified name; + + provide the argument --location on the command line. + + --plugin=PLUGIN + The plugin id of the instance resource. + + To set the plugin attribute: + + provide the argument instance on the command line with a fully + specified name; + + provide the argument --plugin on the command line. + +REQUIRED FLAGS + --actions=[actionId=ACTIONID],[curationConfig=CURATIONCONFIG],[scheduleCronExpression=SCHEDULECRONEXPRESSION],[scheduleTimeZone=SCHEDULETIMEZONE],[serviceAccount=SERVICEACCOUNT] + Required, The action status for the plugin instance. + + actionId + This should map to one of the [action + id][google.cloud.apihub.v1.PluginActionConfig.id] specified in + [actions_config][google.cloud.apihub.v1.Plugin.actions_config] in + the plugin. + + curationConfig + This configuration should be provided if the plugin action is + publishing data to API hub curate layer. + + curationType + The curation type for this plugin instance. + + customCuration + Custom curation information for this plugin instance. + + curation + The unique name of the curation resource. This will be the + name of the curation resource in the format: + projects/{project}/locations/{location}/curations/{curation}. + + scheduleCronExpression + The schedule for this plugin instance action. This can only be set + if the plugin supports API_HUB_SCHEDULE_TRIGGER mode for this + action. + + scheduleTimeZone + The time zone for the schedule cron expression. If not provided, + UTC will be used. + + serviceAccount + The service account used to publish data. Note, the service account + will only be accepted for non-Google Cloud plugins like OPDK. + + Shorthand Example: + + --actions=actionId=string,curationConfig={curationType=string,customCuration={curation=string}},scheduleCronExpression=string,scheduleTimeZone=string,serviceAccount=string --actions=actionId=string,curationConfig={curationType=string,customCuration={curation=string}},scheduleCronExpression=string,scheduleTimeZone=string,serviceAccount=string + + JSON Example: + + --actions='[{"actionId": "string", "curationConfig": {"curationType": "string", "customCuration": {"curation": "string"}}, "scheduleCronExpression": "string", "scheduleTimeZone": "string", "serviceAccount": "string"}]' + + File Example: + + --actions=path_to_file.(yaml|json) + + --display-name=DISPLAY_NAME + The display name for this plugin instance. Max length is 255 + characters. + +OPTIONAL FLAGS + --additional-config=[ADDITIONAL_CONFIG,...] + The additional information for this plugin instance corresponding to + the additional config template of the plugin. This information will be + sent to plugin hosting service on each call to plugin hosted service. + The key will be the config_variable_template.display_name to uniquely + identify the config variable. + + KEY + Sets KEY value. + + VALUE + Sets VALUE value. + + boolValue + The config variable value in case of config variable of type + boolean. + + enumValue + The config variable value in case of config variable of type + enum. + + description + Description of the option. + + displayName + Display name of the option. + + id + Id of the option. + + intValue + The config variable value in case of config variable of type + integer. + + multiIntValues + The config variable value in case of config variable of type + multi integer. + + values + The config variable value of data type multi int. + + multiSelectValues + The config variable value in case of config variable of type + multi select. + + values + The config variable value of data type multi select. + + description + Description of the option. + + displayName + Display name of the option. + + id + Id of the option. + + multiStringValues + The config variable value in case of config variable of type + multi string. + + values + The config variable value of data type multi string. + + secretValue + The config variable value in case of config variable of type + secret. + + secretVersion + The resource name of the secret version in the format, + format as: projects/*/secrets/*/versions/*. + + stringValue + The config variable value in case of config variable of type + string. + + Shorthand Example: + + --additional-config=string={boolValue=boolean,enumValue={description=string,displayName=string,id=string},intValue=int,multiIntValues={values=[int]},multiSelectValues={values=[{description=string,displayName=string,id=string}]},multiStringValues={values=[string]},secretValue={secretVersion=string},stringValue=string} + + JSON Example: + + --additional-config='{"string": {"boolValue": boolean, "enumValue": {"description": "string", "displayName": "string", "id": "string"}, "intValue": int, "multiIntValues": {"values": [int]}, "multiSelectValues": {"values": [{"description": "string", "displayName": "string", "id": "string"}]}, "multiStringValues": {"values": ["string"]}, "secretValue": {"secretVersion": "string"}, "stringValue": "string"}}' + + File Example: + + --additional-config=path_to_file.(yaml|json) + + --async + Return immediately, without waiting for the operation in progress to + complete. + + --source-environments-config=[SOURCE_ENVIRONMENTS_CONFIG,...] + The source environment's config present in the gateway instance linked + to the plugin instance. The key is the source_environment name from the + SourceEnvironment message. + + KEY + Sets KEY value. + + VALUE + Sets VALUE value. + + createTime + The time at which the environment was created at the source. + + sourceEnvironment + The name of the environment at the source. This should map to + [Deployment][google.cloud.apihub.v1.SourceEnvironment.source_environment]. + + sourceEnvironmentUri + The location where additional information about source + environments can be found. The location should be relative path + of the environment manifest with respect to a plugin instance. + + updateTime + The time at which the environment was last updated at the + source. + + Shorthand Example: + + --source-environments-config=string={createTime=string,sourceEnvironment=string,sourceEnvironmentUri=string,updateTime=string} + + JSON Example: + + --source-environments-config='{"string": {"createTime": "string", "sourceEnvironment": "string", "sourceEnvironmentUri": "string", "updateTime": "string"}}' + + File Example: + + --source-environments-config=path_to_file.(yaml|json) + + --source-project-id=SOURCE_PROJECT_ID + The source project id of the plugin instance. This will be the id of + runtime project in case of Google Cloud based plugins and org id in + case of non-Google Cloud based plugins. This field will be a required + field for Google provided on-ramp plugins. + + AuthConfig represents the authentication information. + + --auth-config-type=AUTH_CONFIG_TYPE + The authentication type. AUTH_CONFIG_TYPE must be one of: + + api-key + API Key authentication. + google-service-account + Google service account authentication. + no-auth + No authentication. + oauth2-client-credentials + Oauth 2.0 client credentials grant authentication. + user-password + Username and password authentication. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + Arguments for the config. + + At most one of these can be specified: + + Config for authentication with API key. + + --api-key-config-http-element-location=API_KEY_CONFIG_HTTP_ELEMENT_LOCATION + The location of the API key. The default value is QUERY. + API_KEY_CONFIG_HTTP_ELEMENT_LOCATION must be one of: + + body + Element is in the HTTP request body. + cookie + Element is in the HTTP request cookie. + header + Element is in the HTTP request header. + path + Element is in the HTTP request path. + query + Element is in the HTTP request query. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --api-key-config-name=API_KEY_CONFIG_NAME + The parameter name of the API key. E.g. If the API request is + "https://example.com/act?api_key=", "api_key" would be the + parameter name. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + Secret provides a reference to entries in Secret Manager. + + This must be specified. + + --api-key-config-secret-version=API_KEY_CONFIG_SECRET_VERSION + The resource name of the secret version in the format, format as: + projects/*/secrets/*/versions/*. + + Parameters to support Oauth 2.0 client credentials grant authentication. + See https://tools.ietf.org/html/rfc6749#section-1.3.4 for more details. + + --oauth2-client-credentials-config-id=OAUTH2_CLIENT_CREDENTIALS_CONFIG_ID + The client identifier. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + Secret provides a reference to entries in Secret Manager. + + This must be specified. + + --oauth2-client-credentials-config-secret-version=OAUTH2_CLIENT_CREDENTIALS_CONFIG_SECRET_VERSION + The resource name of the secret version in the format, format as: + projects/*/secrets/*/versions/*. + + Parameters to support Username and Password Authentication. + + Secret provides a reference to entries in Secret Manager. + + This must be specified. + + --user-password-config-secret-version=USER_PASSWORD_CONFIG_SECRET_VERSION + The resource name of the secret version in the format, format as: + projects/*/secrets/*/versions/*. + + --user-password-config-username=USER_PASSWORD_CONFIG_USERNAME + Username. + +GCLOUD WIDE FLAGS + These flags are available to all commands: --access-token-file, --account, + --billing-project, --configuration, --flags-file, --flatten, --format, + --help, --impersonate-service-account, --log-http, --project, --quiet, + --trace-token, --user-output-enabled, --verbosity. + + Run $ gcloud help for details. + +API REFERENCE + This command uses the apihub/v1 API. The full documentation for this API + can be found at: + https://cloud.google.com/apigee/docs/api-hub/what-is-api-hub diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-oracle-database-db-systems-create.txt b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-oracle-database-db-systems-create.txt new file mode 100644 index 00000000000..a05cdc046c4 --- /dev/null +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gcloud/585.0.0/gcloud-oracle-database-db-systems-create.txt @@ -0,0 +1,540 @@ +NAME + gcloud oracle-database db-systems create - create a new DbSystem + +SYNOPSIS + gcloud oracle-database db-systems create DB_SYSTEM + --display-name=DISPLAY_NAME --odb-subnet=ODB_SUBNET [--async] + [--gcp-oracle-zone=GCP_ORACLE_ZONE] [--labels=[LABELS,...]] + [--location=LOCATION] [--odb-network=ODB_NETWORK] + [--request-id=REQUEST_ID] [--secret=SECRET] + [[--properties-compute-count=PROPERTIES_COMPUTE_COUNT + --properties-database-edition=PROPERTIES_DATABASE_EDITION + --properties-initial-data-storage-size-gb=PROPERTIES_INITIAL_DATA_STORAGE_SIZE_GB --properties-license-model=PROPERTIES_LICENSE_MODEL --properties-shape=PROPERTIES_SHAPE --properties-ssh-public-keys=[PROPERTIES_SSH_PUBLIC_KEYS, + ...] + : --db-system-options-storage-management=DB_SYSTEM_OPTIONS_STORAGE_MANAGEMENT --properties-compute-model=PROPERTIES_COMPUTE_MODEL --properties-data-storage-size-gb=PROPERTIES_DATA_STORAGE_SIZE_GB --properties-domain=PROPERTIES_DOMAIN --properties-hostname-prefix=PROPERTIES_HOSTNAME_PREFIX --properties-memory-size-gb=PROPERTIES_MEMORY_SIZE_GB --properties-node-count=PROPERTIES_NODE_COUNT --properties-private-ip=PROPERTIES_PRIVATE_IP --properties-reco-storage-size-gb=PROPERTIES_RECO_STORAGE_SIZE_GB --data-collection-options-is-diagnostics-events-enabled --data-collection-options-is-incident-logs-enabled [--db-home-version=DB_HOME_VERSION (--database-admin-password=DATABASE_ADMIN_PASSWORD --database-admin-password-secret-version=DATABASE_ADMIN_PASSWORD_SECRET_VERSION --database-character-set=DATABASE_CHARACTER_SET --database-db-home-name=DATABASE_DB_HOME_NAME --database-db-name=DATABASE_DB_NAME --database-db-unique-name=DATABASE_DB_UNIQUE_NAME --database-id=DATABASE_ID --database-name=DATABASE_NAME --database-ncharacter-set=DATABASE_NCHARACTER_SET --database-pluggable-id=DATABASE_PLUGGABLE_ID --database-pluggable-name=DATABASE_PLUGGABLE_NAME --database-tde-wallet-password=DATABASE_TDE_WALLET_PASSWORD --database-tde-wallet-password-secret-version=DATABASE_TDE_WALLET_PASSWORD_SECRET_VERSION [--properties-db-version=PROPERTIES_DB_VERSION : --db-backup-config-auto-enabled --db-backup-config-auto-full-day=DB_BACKUP_CONFIG_AUTO_FULL_DAY --db-backup-config-auto-full-window=DB_BACKUP_CONFIG_AUTO_FULL_WINDOW --db-backup-config-auto-incremental-window=DB_BACKUP_CONFIG_AUTO_INCREMENTAL_WINDOW --db-backup-config-deletion-policy=DB_BACKUP_CONFIG_DELETION_POLICY --db-backup-config-destination-details=[type=TYPE] --db-backup-config-retention-period-days=DB_BACKUP_CONFIG_RETENTION_PERIOD_DAYS]) : --db-home-display-name=DB_HOME_DISPLAY_NAME --db-home-is-unified-auditing-enabled] --time-zone-id=TIME_ZONE_ID --time-zone-version=TIME_ZONE_VERSION]] + [GCLOUD_WIDE_FLAG ...] + +DESCRIPTION + Create a new DbSystem. + +EXAMPLES + Choose an available db-version in your location by running gcloud + oracle-database db-versions list --location=us-east4. Choose an available + db-system-initial-storage-size in your location by running gcloud + oracle-database db-system-initial-storage-sizes list --location=us-east4. + Choose an available shape in your location by running gcloud + oracle-database db-system-shapes list --location=us-east4. To create + DbSystem with id my-db-system in the location us-east4 with display-name my + db system, run: + + $ gcloud oracle-database db-systems create my-db-system \ + --location=us-east4 --display-name="my db system" \ + --properties-db-home-db-version=xx.0.0.0 \ + --properties-db-system-initial-storage-size=1000 \ + --properties-shape=VM.FOO + + To create a DbSystem with pluggable database parameters specified, run: + + $ gcloud oracle-database db-systems create my-db-system \ + --location=us-east4 --display-name="my db system" \ + --properties-db-home-db-version=xx.0.0.0 \ + --properties-db-system-initial-storage-size=1000 \ + --properties-shape=VM.FOO --database-pluggable-id=my-pdb-id \ + --database-pluggable-name=my_pdb_name + +POSITIONAL ARGUMENTS + DbSystem resource - Identifier. The name of the DbSystem resource in the + following format: + projects/{project}/locations/{region}/dbSystems/{db_system} This + represents a Cloud resource. (NOTE) Some attributes are not given + arguments in this group but can be set in other ways. + + To set the project attribute: + * provide the argument db_system on the command line with a fully + specified name; + * provide the argument --project on the command line; + * set the property core/project. + + To set the location attribute: + * provide the argument db_system on the command line with a fully + specified name; + * provide the argument --location on the command line. + + This must be specified. + + DB_SYSTEM + ID of the dbSystem or fully qualified identifier for the dbSystem. + + To set the db_system attribute: + + provide the argument db_system on the command line. + +REQUIRED FLAGS + --display-name=DISPLAY_NAME + The display name for the System db. The name does not have to be unique + within your project. + + OdbSubnet resource - The name of the OdbSubnet associated with the + DbSystem for IP allocation. Format: + projects/{project}/locations/{location}/odbNetworks/{odb_network}/odbSubnets/{odb_subnet} + This represents a Cloud resource. (NOTE) Some attributes are not given + arguments in this group but can be set in other ways. + + To set the project attribute: + * provide the argument --odb-subnet on the command line with a fully + specified name; + * provide the argument --project on the command line; + * set the property core/project. + + To set the location attribute: + * provide the argument --odb-subnet on the command line with a fully + specified name; + * provide the argument --location on the command line. + + To set the odb-network attribute: + * provide the argument --odb-subnet on the command line with a fully + specified name; + * provide the argument --odb-network on the command line. + + This must be specified. + + --odb-subnet=ODB_SUBNET + ID of the odbSubnet or fully qualified identifier for the odbSubnet. + + To set the odb-subnet attribute: + + provide the argument --odb-subnet on the command line. + +OPTIONAL FLAGS + --async + Return immediately, without waiting for the operation in progress to + complete. + + --gcp-oracle-zone=GCP_ORACLE_ZONE + The GCP Oracle zone where Oracle DbSystem is hosted. Example: + us-east4-b-r2. If not specified, the system will pick a zone based on + availability. + + --labels=[LABELS,...] + The labels or tags associated with the DbSystem. + + KEY + Keys must start with a lowercase character and contain only hyphens + (-), underscores (_), lowercase characters, and numbers. + + VALUE + Values must contain only hyphens (-), underscores (_), lowercase + characters, and numbers. + + Shorthand Example: + + --labels=string=string + + JSON Example: + + --labels='{"string": "string"}' + + File Example: + + --labels=path_to_file.(yaml|json) + + --location=LOCATION + For resources [db_system, odb-network, odb-subnet], provides fallback + value for resource location attribute. When the resource's full URI + path is not provided, location will fallback to this flag value. + + --odb-network=ODB_NETWORK + For resources [odb-network, odb-subnet], provides fallback value for + resource odb-network attribute. When the resource's full URI path is + not provided, odb-network will fallback to this flag value. + + --request-id=REQUEST_ID + An optional request ID to identify requests. Specify a unique request + ID so that if you must retry your request, the server will know to + ignore the request if it has already been completed. The server will + guarantee that for at least 60 minutes since the first request. + + For example, consider a situation where you make an initial request and + the request times out. If you make the request again with the same + request ID, the server can check if original operation with the same + request ID was received, and if so, will ignore the second request. + This prevents clients from accidentally creating duplicate commitments. + + The request ID must be a valid UUID with the exception that zero UUID + is not supported (00000000-0000-0000-0000-000000000000). + + --secret=SECRET + For resources [database-admin-password-secret-version, + database-tde-wallet-password-secret-version], provides fallback value + for resource secret attribute. When the resource's full URI path is not + provided, secret will fallback to this flag value. + + The properties of a DbSystem. + + --properties-compute-count=PROPERTIES_COMPUTE_COUNT + The number of CPU cores to enable for the DbSystem. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --properties-database-edition=PROPERTIES_DATABASE_EDITION + The database edition of the DbSystem. PROPERTIES_DATABASE_EDITION must + be one of: + + enterprise-edition + The database edition is Enterprise. + enterprise-edition-high-performance + The database edition is Enterprise Edition. + standard-edition + The database edition is Standard. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --properties-initial-data-storage-size-gb=PROPERTIES_INITIAL_DATA_STORAGE_SIZE_GB + The initial data storage size in GB. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --properties-license-model=PROPERTIES_LICENSE_MODEL + The license model of the DbSystem. PROPERTIES_LICENSE_MODEL must be one + of: + + bring-your-own-license + The license model is bring your own license. + license-included + The license model is included. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --properties-shape=PROPERTIES_SHAPE + Shape of DB System. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --properties-ssh-public-keys=[PROPERTIES_SSH_PUBLIC_KEYS,...] + SSH public keys to be stored with the DbSystem. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + Details of the DbSystem Options. + + --db-system-options-storage-management=DB_SYSTEM_OPTIONS_STORAGE_MANAGEMENT + The storage option used in DB system. + DB_SYSTEM_OPTIONS_STORAGE_MANAGEMENT must be one of: + + asm + Automatic storage management. This option is not supported. Only + LVM is supported. + lvm + Logical Volume management. + + --properties-compute-model=PROPERTIES_COMPUTE_MODEL + The compute model of the DbSystem. PROPERTIES_COMPUTE_MODEL must be one + of: + + ecpu + The compute model is virtual. + ocpu + Deprecated: This option is not supported. Please use ECPU instead. + The compute model is physical. + + --properties-data-storage-size-gb=PROPERTIES_DATA_STORAGE_SIZE_GB + The data storage size in GB that is currently available to DbSystems. + The value is same as initial_data_storage_size_gb. This can be modified + from OCI console. + + --properties-domain=PROPERTIES_DOMAIN + The host domain name of the DbSystem. + + --properties-hostname-prefix=PROPERTIES_HOSTNAME_PREFIX + Prefix for DB System host names. + + --properties-memory-size-gb=PROPERTIES_MEMORY_SIZE_GB + The memory size in GB. This value can not be set and is automatically + calculated based on the number of ECPUs allocated to the DbSystem. + + --properties-node-count=PROPERTIES_NODE_COUNT + The number of nodes to launch for a virtual machine DbSystem. By + default this will be set to 1. + + --properties-private-ip=PROPERTIES_PRIVATE_IP + The private IP address of the DbSystem. + + --properties-reco-storage-size-gb=PROPERTIES_RECO_STORAGE_SIZE_GB + The reco/redo storage size in GB. The value for recovery storage size + is based on the available data storage size. + + Data collection options for DbSystem. + + --data-collection-options-is-diagnostics-events-enabled + Indicates whether to enable data collection for diagnostics. + + --data-collection-options-is-incident-logs-enabled + Indicates whether to enable incident logs and trace collection. + + Details of the Database Home resource. + + --db-home-version=DB_HOME_VERSION + A valid Oracle Database version. For a list of supported versions, use + the ListDbVersions operation. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + --db-home-display-name=DB_HOME_DISPLAY_NAME + The display name for the Database Home. The name does not have to be + unique within your project. + + --db-home-is-unified-auditing-enabled + Whether unified auditing is enabled for the Database Home. + + Details of the Database resource. + https://docs.oracle.com/en-us/iaas/api/#/en/database/20160918/Database/ + + At least one of these must be specified: + + --database-admin-password=DATABASE_ADMIN_PASSWORD + The password for the default ADMIN user. Note: Only one of + admin_password_secret_version or admin_password can be populated. + + SecretVersion resource - The resource name of a secret version in Secret + Manager which contains the database admin user's password. Format: + projects/{project}/secrets/{secret}/versions/{version}. Note: Only one + of admin_password_secret_version or admin_password can be populated. + This represents a Cloud resource. (NOTE) Some attributes are not given + arguments in this group but can be set in other ways. + + To set the project attribute: + + provide the argument --database-admin-password-secret-version on + the command line with a fully specified name; + + provide the argument --project on the command line; + + set the property core/project. + + To set the secret attribute: + + provide the argument --database-admin-password-secret-version on + the command line with a fully specified name; + + provide the argument --secret on the command line. + + --database-admin-password-secret-version=DATABASE_ADMIN_PASSWORD_SECRET_VERSION + ID of the secretVersion or fully qualified identifier for the + secretVersion. + + To set the secret-version attribute: + + provide the argument --database-admin-password-secret-version on + the command line. + + --database-character-set=DATABASE_CHARACTER_SET + The character set for the database. The default is AL32UTF8. + + --database-db-home-name=DATABASE_DB_HOME_NAME + The name of the DbHome resource associated with the Database. + + --database-db-name=DATABASE_DB_NAME + The database name. The name must begin with an alphabetic character + and can contain a maximum of eight alphanumeric characters. Special + characters are not permitted. + + --database-db-unique-name=DATABASE_DB_UNIQUE_NAME + The DB_UNIQUE_NAME of the Oracle Database being backed up. + + --database-id=DATABASE_ID + The database ID of the Database. + + --database-name=DATABASE_NAME + Identifier. The name of the Database resource in the following + format: projects/{project}/locations/{region}/databases/{database} + + --database-ncharacter-set=DATABASE_NCHARACTER_SET + The national character set for the database. The default is + AL16UTF16. + + --database-pluggable-id=DATABASE_PLUGGABLE_ID + The ID of the pluggable database associated with the Database. The ID + must be unique within the project and location. + + --database-pluggable-name=DATABASE_PLUGGABLE_NAME + The pluggable database associated with the Database. The name must + begin with an alphabetic character and can contain a maximum of + thirty alphanumeric characters. + + --database-tde-wallet-password=DATABASE_TDE_WALLET_PASSWORD + The TDE wallet password for the database. Note: Only one of + tde_wallet_password_secret_version or tde_wallet_password can be + populated. + + SecretVersion resource - The resource name of a secret version in Secret + Manager which contains the TDE wallet password for the database. Format: + projects/{project}/secrets/{secret}/versions/{version}. Note: Only one + of tde_wallet_password_secret_version or tde_wallet_password can be + populated. This represents a Cloud resource. (NOTE) Some attributes are + not given arguments in this group but can be set in other ways. + + To set the project attribute: + + provide the argument --database-tde-wallet-password-secret-version + on the command line with a fully specified name; + + provide the argument --project on the command line; + + set the property core/project. + + To set the secret attribute: + + provide the argument --database-tde-wallet-password-secret-version + on the command line with a fully specified name; + + provide the argument --secret on the command line. + + --database-tde-wallet-password-secret-version=DATABASE_TDE_WALLET_PASSWORD_SECRET_VERSION + ID of the secretVersion or fully qualified identifier for the + secretVersion. + + To set the secret-version attribute: + + provide the argument + --database-tde-wallet-password-secret-version on the command line. + + The properties of a Database. + + --properties-db-version=PROPERTIES_DB_VERSION + The Oracle Database version. + + This flag argument must be specified if any of the other arguments in + this group are specified. + + Backup Options for the Database. + + --db-backup-config-auto-enabled + If set to true, enables automatic backups on the database. + + --db-backup-config-auto-full-day=DB_BACKUP_CONFIG_AUTO_FULL_DAY + The day of the week on which the full backup should be performed on + the database. If no value is provided, it will default to Sunday. + DB_BACKUP_CONFIG_AUTO_FULL_DAY must be one of: + + friday + Friday + monday + Monday + saturday + Saturday + sunday + Sunday + thursday + Thursday + tuesday + Tuesday + wednesday + Wednesday + + --db-backup-config-auto-full-window=DB_BACKUP_CONFIG_AUTO_FULL_WINDOW + The window in which the full backup should be performed on the + database. If no value is provided, the default is anytime. + DB_BACKUP_CONFIG_AUTO_FULL_WINDOW must be one of: + + slot-eight + 2:00 PM - 4:00 PM + slot-eleven + 8:00 PM - 10:00 PM + slot-five + 8:00 AM - 10:00 AM + slot-four + 6:00 AM - 8:00 AM + slot-nine + 4:00 PM - 6:00 PM + slot-one + 12:00 AM - 2:00 AM + slot-seven + 12:00 PM - 2:00 PM + slot-six + 10:00 AM - 12:00 PM + slot-ten + 6:00 PM - 8:00 PM + slot-three + 4:00 AM - 6:00 AM + slot-twelve + 10:00 PM - 12:00 AM + slot-two + 2:00 AM - 4:00 AM + + --db-backup-config-auto-incremental-window=DB_BACKUP_CONFIG_AUTO_INCREMENTAL_WINDOW + The window in which the incremental backup should be performed on the + database. If no value is provided, the default is anytime except the + auto full backup day. DB_BACKUP_CONFIG_AUTO_INCREMENTAL_WINDOW must + be one of: + + slot-eight + 2:00 PM - 4:00 PM + slot-eleven + 8:00 PM - 10:00 PM + slot-five + 8:00 AM - 10:00 AM + slot-four + 6:00 AM - 8:00 AM + slot-nine + 4:00 PM - 6:00 PM + slot-one + 12:00 AM - 2:00 AM + slot-seven + 12:00 PM - 2:00 PM + slot-six + 10:00 AM - 12:00 PM + slot-ten + 6:00 PM - 8:00 PM + slot-three + 4:00 AM - 6:00 AM + slot-twelve + 10:00 PM - 12:00 AM + slot-two + 2:00 AM - 4:00 AM + + --db-backup-config-deletion-policy=DB_BACKUP_CONFIG_DELETION_POLICY + This defines when the backups will be deleted after Database + termination. DB_BACKUP_CONFIG_DELETION_POLICY must be one of: + + delete-after-retention-period + Keeps the backups as per the policy defined for database backups. + delete-immediately + Keeps the backup for predefined time i.e. 72 hours and then + delete permanently. + + --db-backup-config-destination-details=[type=TYPE] + Details of the database backup destinations. + + type + The type of the database backup destination. + + Shorthand Example: + + --db-backup-config-destination-details=type=string --db-backup-config-destination-details=type=string + + JSON Example: + + --db-backup-config-destination-details='[{"type": "string"}]' + + File Example: + + --db-backup-config-destination-details=path_to_file.(yaml|json) + + --db-backup-config-retention-period-days=DB_BACKUP_CONFIG_RETENTION_PERIOD_DAYS + The number of days an automatic backup is retained before being + automatically deleted. This value determines the earliest point in + time to which a database can be restored. Min: 1, Max: 60. + + Represents a time zone from the IANA Time Zone Database + (https://www.iana.org/time-zones). + + --time-zone-id=TIME_ZONE_ID + IANA Time Zone Database time zone. For example "America/New_York". + + --time-zone-version=TIME_ZONE_VERSION + IANA Time Zone Database version number. For example "2019a". + +GCLOUD WIDE FLAGS + These flags are available to all commands: --access-token-file, --account, + --billing-project, --configuration, --flags-file, --flatten, --format, + --help, --impersonate-service-account, --log-http, --project, --quiet, + --trace-token, --user-output-enabled, --verbosity. + + Run $ gcloud help for details. + +API REFERENCE + This command uses the oracledatabase/v1 API. The full documentation for + this API can be found at: https://cloud.google.com/oracle/database/docs diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GcloudPeerResourceValidationTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GcloudPeerResourceValidationTests.cs new file mode 100644 index 00000000000..4cb8218cb9e --- /dev/null +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GcloudPeerResourceValidationTests.cs @@ -0,0 +1,71 @@ +using System.ComponentModel.DataAnnotations; +using ModularPipelines.OptionsGenerator.Models; +using ModularPipelines.OptionsGenerator.Tests.Scrapers.Cli; + +namespace ModularPipelines.OptionsGenerator.Tests.Generators; + +public partial class RequiredConstructorValidationTests +{ + [Test] + public async Task Gcloud_Authentication_Branches_Remain_Independent() + { + var command = await GcloudCapturedSemanticsTests.Scrape("apihub plugins instances create"); + await Assert.That(command.Options.Single(option => option.PropertyName == "UserPasswordConfigUsername").Description) + .DoesNotContain("Oauth"); + var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("AuthConfigType")); + await ValidateCapturedGroup(command, group, + [ + ("", true), + ("AuthConfigType", true), + ("AuthConfigType,UserPasswordConfigSecretVersion,UserPasswordConfigUsername", true), + ("AuthConfigType,Oauth2ClientCredentialsConfigId,Oauth2ClientCredentialsConfigSecretVersion", true), + ("AuthConfigType,ApiKeyConfigHttpElementLocation,ApiKeyConfigName,ApiKeyConfigSecretVersion", true), + ("UserPasswordConfigSecretVersion,UserPasswordConfigUsername", false), + ("AuthConfigType,Oauth2ClientCredentialsConfigSecretVersion", false), + ("AuthConfigType,Oauth2ClientCredentialsConfigId", false), + ("AuthConfigType,UserPasswordConfigUsername", false), + ("AuthConfigType,Oauth2ClientCredentialsConfigId,Oauth2ClientCredentialsConfigSecretVersion,UserPasswordConfigSecretVersion,UserPasswordConfigUsername", false), + ]); + } + + [Test] + public async Task Gcloud_Required_Database_Bundle_Retains_Resource_Members() + { + var command = await GcloudCapturedSemanticsTests.Scrape("oracle-database db-systems create"); + var group = Descendants(command.RequiredAlternativeGroups) + .Single(group => group.Members.Any(member => member.PropertyName == "DbHomeVersion")); + await ValidateCapturedGroup(command, group, + [ + ("", true), + ("DbHomeVersion", false), + ("DbHomeVersion,DatabaseAdminPassword", true), + ("DbHomeVersion,DatabaseAdminPasswordSecretVersion", true), + ("DbHomeVersion,DatabaseDbName", true), + ("DatabaseAdminPasswordSecretVersion", false), + ]); + } + + private static IEnumerable Descendants(IEnumerable groups) => + groups.SelectMany(group => new[] { group }.Concat(Descendants(group.Groups))); + + private static async Task ValidateCapturedGroup(CliCommandDefinition command, CliRequiredAlternativeGroup group, + (string Properties, bool Valid)[] cases) + { + var options = command.Options.Where(option => group.PropertyNames.Contains(option.PropertyName)).ToList(); + var generated = await Generate(options, alternativeGroups: [group]); + const string secretAttribute = "namespace ModularPipelines.Secrets { public sealed class SecretValueAttribute : System.Attribute; }"; + var optionsType = Compile(generated, secretAttribute).GetType("ModularPipelines.Tool.Options.ToolRunOptions")!; + foreach (var (properties, valid) in cases) + { + var instance = Activator.CreateInstance(optionsType)!; + foreach (var name in properties.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + optionsType.GetProperty(name)!.SetValue(instance, "value"); + } + + var errors = new List(); + await Assert.That(Validator.TryValidateObject(instance, new(instance), errors, true)) + .IsEqualTo(valid).Because($"Selected {properties}: {string.Join("; ", errors)}"); + } + } +} diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs index 64ead867133..0fcec647bcf 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs @@ -9,6 +9,28 @@ namespace ModularPipelines.OptionsGenerator.Tests.Scrapers; public class UsageSynopsisParserTests { + [Test] + public async Task Optional_Selectors_Preserve_Whole_Alternative_Branches() + { + var groups = UsageSynopsisParser.GetOptionalResourceOptionGroups( + "tool run [--kind=KIND : --id=ID --secret=SECRET | --user=USER --password=PASSWORD]") + .ToArray(); + await Assert.That(groups).Count().IsEqualTo(3); + await Assert.That(groups[1]).IsEquivalentTo(["--id", "--secret"]); + await Assert.That(groups[2]).IsEquivalentTo(["--user", "--password"]); + } + + [Test] + public async Task Alternative_Branches_Preserve_Explicitly_Optional_Nested_Resources() + { + var groups = UsageSynopsisParser.GetOptionalResourceOptionGroups( + "tool run [--kind=KIND : --id=ID [--secret=SECRET : --secret-project=PROJECT] | --user=USER]") + .ToArray(); + await Assert.That(groups.Any(group => group.SetEquals(["--id", "--secret", "--secret-project"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--secret", "--secret-project"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--secret-project"]))).IsTrue(); + } + [Test] [Arguments("(RESOURCE --parent=PARENT)")] [Arguments("(RESOURCE --parent=PARENT [--optional=VALUE])")] diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CliArgumentGroupParser.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CliArgumentGroupParser.cs index e90153b0d2a..b529748eb4b 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CliArgumentGroupParser.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CliArgumentGroupParser.cs @@ -258,10 +258,13 @@ private static void BeginArgumentGroup( bool namedBundle = false, int? headingIndentation = null) { var current = stack.Peek(); - if (indentation == current.Indentation && current.IsNamedBundle && preludeStartsGroup && !namedBundle) + var resourceWithinChoice = Classify(prelude).HasFlag(CliArgumentGroupKind.Resource) + && (Classify(current.Description) & (CliArgumentGroupKind.AtLeastOne | CliArgumentGroupKind.AtMostOne)) != 0; + if (indentation == current.Indentation && (current.IsNamedBundle || resourceWithinChoice) + && preludeStartsGroup && !namedBundle) { - // Resource prose can sit at the same depth as its provider heading. - // End this resource before the next peer flag, while retaining the provider. + // Resource prose can share a provider's or choice's flag indentation. + // End this resource before the next peer flag while retaining its parent. indentation++; } if (indentation > current.Indentation) @@ -443,7 +446,7 @@ private sealed record ParsedArgumentLine( [GeneratedRegex(@"^\s*\S\s+provide the argument\s*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] private static partial Regex WrappedArgumentReferencePattern(); - [GeneratedRegex(@"^(?:(?:Defines the )?configuration for|(?:Bearer token|Basic) authentication with)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + [GeneratedRegex(@"^(?:(?:Defines the )?configuration for|config for|parameters to support|(?:Bearer token|Basic) authentication with)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] private static partial Regex NamedBundleHeadingPattern(); [GeneratedRegex(@"^(?:(?:[\w-]+\s+)*configuration for\b|options for\b)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs index 620b9560e6d..54972ae76e3 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs @@ -1392,9 +1392,15 @@ static IEnumerable> Visit(string token, bool optional) // Selectors after ':' belong to the optional side of this resource bundle. // Their own nested requirements still apply when they are selected. - foreach (var selector in tokens.Skip(colon + 1).Where(item => item is not (":" or "|"))) + var selectors = tokens.Skip(colon + 1).ToArray(); + // Alternatives select whole branches. Splitting their members would + // make required children within the selected branch optional too. + var optionalSelectors = selectors.Contains("|") + ? SplitTopLevelAlternatives(string.Join(" ", selectors)) + : selectors.Where(item => item != ":"); + foreach (var selector in optionalSelectors) { - yield return EnumerateInlineOptionSwitches([selector]).ToHashSet(StringComparer.Ordinal); + yield return EnumerateInlineOptionSwitches(TokenizeOptionGroup(selector)).ToHashSet(StringComparer.Ordinal); } } From 3f282d54d9384ef675fa75403f8fded98a227ace Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:43:37 +0100 Subject: [PATCH 2/4] fix(generator): retain common optional selectors outside choice branches --- .../Scrapers/Cli/GcloudCliScraper.cs | 5 ++-- .../Scrapers/Cli/UsageSynopsisParser.cs | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GcloudCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GcloudCliScraper.cs index fa002f7868e..be6adac09de 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GcloudCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GcloudCliScraper.cs @@ -312,8 +312,9 @@ private static List ExtractFromSection(string helpText, string sectionNa var optionalResourceGroups = UsageSynopsisParser.GetOptionalResourceOptionGroups(usage.Synopsis).ToArray(); foreach (var (name, content) in ExtractSections(helpText, "FLAGS", "REQUIRED FLAGS", "OPTIONAL FLAGS", "POSITIONAL ARGUMENTS")) { - var argumentGroup = MarkOptionalResourceGroups( - ParseSectionArgumentGroup(name, content, optionalResourceGroups), optionalResourceGroups); + var parsedGroup = ParseSectionArgumentGroup(name, content, optionalResourceGroups); + var documentedOptionalGroups = UsageSynopsisParser.GetOptionalResourceOptionGroups(usage.Synopsis, [parsedGroup]).ToArray(); + var argumentGroup = MarkOptionalResourceGroups(parsedGroup, documentedOptionalGroups); argumentGroups.Add(argumentGroup); sections.Add((name, argumentGroup)); foreach (var argument in argumentGroup.FlattenArguments().Where(argument => !argument.IsPositional)) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs index 54972ae76e3..f45d49b187a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/UsageSynopsisParser.cs @@ -1369,11 +1369,12 @@ internal static string DeferDocumentedOptionGroups(string synopsis, IReadOnlyLis return result.Append(synopsis, offset, synopsis.Length - offset).ToString(); } - internal static IEnumerable> GetOptionalResourceOptionGroups(string? synopsis) + internal static IEnumerable> GetOptionalResourceOptionGroups(string? synopsis, + IReadOnlyList? documentedGroups = null) { return synopsis is null ? [] : Tokenize(synopsis).SelectMany(token => Visit(token, false)); - static IEnumerable> Visit(string token, bool optional) + IEnumerable> Visit(string token, bool optional) { if (!IsWrapped(token)) { @@ -1400,7 +1401,24 @@ static IEnumerable> Visit(string token, bool optional) : selectors.Where(item => item != ":"); foreach (var selector in optionalSelectors) { - yield return EnumerateInlineOptionSwitches(TokenizeOptionGroup(selector)).ToHashSet(StringComparer.Ordinal); + var selectorTokens = TokenizeOptionGroup(selector); + var switches = EnumerateInlineOptionSwitches(selectorTokens).ToHashSet(StringComparer.Ordinal); + // gcloud can print common optional selectors before a nested choice + // without wrapping that choice. Keep a whole branch only when its + // documented group confirms the same members. + if (documentedGroups is not null && selectorTokens.Count > 1 + && !EnumerateArgumentGroups(documentedGroups).Any(group => switches.SetEquals( + group.FlattenArguments().Select(argument => argument.SwitchName)))) + { + foreach (var selectorToken in selectorTokens.Where(item => item is not (":" or "|"))) + { + yield return EnumerateInlineOptionSwitches([selectorToken]).ToHashSet(StringComparer.Ordinal); + } + } + else + { + yield return switches; + } } } From e69d577dc427b286e3b9a849aaa2007948fb9a1a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:51:56 +0100 Subject: [PATCH 3/4] test(generator): cover optional selectors around nested choices --- .../Scrapers/UsageSynopsisParserTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs index 0fcec647bcf..60dad9c2d84 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/UsageSynopsisParserTests.cs @@ -9,6 +9,42 @@ namespace ModularPipelines.OptionsGenerator.Tests.Scrapers; public class UsageSynopsisParserTests { + [Test] + public async Task Optional_Common_Selectors_Remain_Independent_Of_Nested_Alternatives() + { + var documentedGroup = new CliArgumentGroup + { + Arguments = + [ + new() { SwitchName = "--host" }, + new() { SwitchName = "--directory" }, + new() { SwitchName = "--certificate" }, + ], + Groups = + [ + new() + { + Kind = CliArgumentGroupKind.AtMostOne, + Groups = + [ + new() { Arguments = [new() { SwitchName = "--token" }] }, + new() { Arguments = [new() { SwitchName = "--username" }, new() { SwitchName = "--password" }] }, + ], + }, + ], + }; + var groups = UsageSynopsisParser.GetOptionalResourceOptionGroups( + "tool run [--host=HOST : --directory=DIR --certificate=CERT --token=TOKEN | [--username=USER : --password=PASSWORD]]", + [documentedGroup]).ToArray(); + + await Assert.That(groups.Any(group => group.SetEquals(["--directory"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--certificate"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--token"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--username", "--password"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--password"]))).IsTrue(); + await Assert.That(groups.Any(group => group.SetEquals(["--directory", "--certificate", "--token"]))).IsFalse(); + } + [Test] public async Task Optional_Selectors_Preserve_Whole_Alternative_Branches() { From 425362ec7ecb05c2cb6a1b87f309e8fb0e5aadb3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:17:34 +0100 Subject: [PATCH 4/4] fix(generator): keep credential location metadata visible --- .../Generators/GeneratorUtilsTests.cs | 5 +++++ .../Scrapers/Cli/GcloudCapturedSemanticsTests.cs | 8 ++++++++ .../TypeDetection/OptionTypeEnhancerTests.cs | 15 +++++++++------ .../Generators/GeneratorUtils.cs | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorUtilsTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorUtilsTests.cs index 872ddadab0e..034ed231361 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorUtilsTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorUtilsTests.cs @@ -895,6 +895,9 @@ public async Task IsSecretOption_Preserves_Secret_Values_With_Numeric_Context(st [Arguments("CredentialAgeDays")] [Arguments("MinimumSessionTokenSize")] [Arguments("SsoTokenBufferMinutes")] + [Arguments("ApiKeyConfigHttpElementLocation")] + [Arguments("TokenLocation")] + [Arguments("CredentialLocation")] public async Task IsSecretOption_Keeps_Credential_Metadata_Visible_Without_Descriptions(string propertyName) { await Assert.That(GeneratorUtils.IsSecretOption(propertyName, false)).IsFalse(); @@ -922,6 +925,8 @@ public async Task IsSecretOption_Returns_False_When_Description_Identifies_A_Pat [Arguments("SecretUser", "Name of the secret user to delete.", false)] [Arguments("Token", "The token value for the user to create.", true)] [Arguments("Token", "The authentication token value used to access the resource.", true)] + [Arguments("SecretLocation", "The secret value to store.", true)] + [Arguments("PrivateKeyLocation", "The private key content to send.", true)] public async Task IsSecretOption_Distinguishes_Material_From_Resource_Identifiers_And_Paths( string propertyName, string description, bool secret) { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/Cli/GcloudCapturedSemanticsTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/Cli/GcloudCapturedSemanticsTests.cs index 4695a4f69cd..b713959e8e6 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/Cli/GcloudCapturedSemanticsTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/Cli/GcloudCapturedSemanticsTests.cs @@ -5,6 +5,14 @@ namespace ModularPipelines.OptionsGenerator.Tests.Scrapers.Cli; public class GcloudCapturedSemanticsTests { + [Test] + public async Task Captured_Api_Key_Http_Location_Is_Not_Secret_Material() + { + var command = await Scrape("apihub plugins instances create"); + var location = command.Options.Single(option => option.PropertyName == "ApiKeyConfigHttpElementLocation"); + await Assert.That(location.IsSecret).IsFalse(); + } + [Test] public async Task Captured_Command_Enums_Preserve_Different_Edition_Values() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/TypeDetection/OptionTypeEnhancerTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/TypeDetection/OptionTypeEnhancerTests.cs index d745a6a251e..7eee5b7432a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/TypeDetection/OptionTypeEnhancerTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/TypeDetection/OptionTypeEnhancerTests.cs @@ -499,9 +499,12 @@ public async Task EnhanceAsync_Removes_Inferred_Secret_From_Boolean_Value_Option } [Test] - [Arguments(false)] - [Arguments(true)] - public async Task Resource_Identifiers_Clear_Inferred_Secrets_But_Respect_Overrides(bool explicitSecret) + [Arguments(false, "TokenAuthUser", "The tokenAuthUser id of the authToken resource.")] + [Arguments(true, "TokenAuthUser", "The tokenAuthUser id of the authToken resource.")] + [Arguments(false, "ApiKeyConfigHttpElementLocation", "The location of the API key. The default value is QUERY.")] + [Arguments(true, "ApiKeyConfigHttpElementLocation", "The location of the API key. The default value is QUERY.")] + public async Task Credential_Metadata_Clears_Inferred_Secrets_But_Respects_Overrides( + bool explicitSecret, string propertyName, string description) { var pipeline = new OptionTypeDetectorPipeline( explicitSecret ? [new FixedDetector(new OptionTypeDetectionResult @@ -511,10 +514,10 @@ public async Task Resource_Identifiers_Clear_Inferred_Secrets_But_Respect_Overri var enhancer = new OptionTypeEnhancer(pipeline, NullLogger.Instance); var tool = CreateTool(new CliOptionDefinition { - SwitchName = "--token-auth-user", - PropertyName = "TokenAuthUser", + SwitchName = "--metadata", + PropertyName = propertyName, CSharpType = "string?", - Description = "The tokenAuthUser id of the authToken resource.", + Description = description, IsSecret = true, }); var enhanced = await enhancer.EnhanceAsync(tool); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 5cc2f327945..e25359e0650 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -1013,6 +1013,7 @@ internal static bool IsSecretMetadataOption(string propertyName, string? descrip } return SecretMetadataSuffixes.Any(suffix => EndsWithIdentifierSegment(propertyName, suffix)) + || (EndsWithIdentifierSegment(propertyName, "Location") && !DescriptionIdentifiesSecretValue(description)) || (!string.IsNullOrWhiteSpace(description) && CountDescriptionPattern().IsMatch(description)); }