Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3831 - New (labs) CLI command for measure new and measure copy #4875

Merged
merged 8 commits into from
Jun 29, 2023
223 changes: 220 additions & 3 deletions src/cli/MeasureUpdateCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,18 @@
#include <utilities/idd/IddEnums.hxx>

#include <fmt/format.h>
#include <fmt/ranges.h>

#include <boost/optional.hpp>
#include <memory>
#include <stdexcept>

#include <algorithm> // For std::transform
#include <cstdlib> // For std::exit
#include <memory> // make_shared
#include <map> // For std::map
#include <stdexcept> // For std::runtime_error
#include <string>
#include <string_view>
#include <vector>

namespace openstudio {
namespace cli {
Expand Down Expand Up @@ -91,6 +100,202 @@ namespace cli {
->option_text("PORT")
->excludes(directoryPathOpt);

{
auto* newMeasureSubCommand = measureCommand->add_subcommand("new", "Create a new measure");

// TODO: this includes 'UtilityMeasure' which I don't think we want to include
std::vector<std::string> measureTypeNames;
{
const auto& m = openstudio::MeasureType::getNames();
std::transform(m.cbegin(), m.cend(), back_inserter(measureTypeNames), [](const auto& x) { return x.second; });
// fmt::print("measureTypeNames={}\n", measureTypeNames);
[[maybe_unused]] auto* measureTypeOpt =
newMeasureSubCommand
->add_option("-t,--type", opt->newMeasureOpts.measureType,
fmt::format("Type of Measure [Default: '{}']: {}", opt->newMeasureOpts.measureType.valueName(), measureTypeNames))
->option_text("measureType")
->check(CLI::IsMember(measureTypeNames));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throughout the options, I'm going to validate input.

eg try to pass measure new --type Badval and you'll get:

--type: Badval not in {ModelMeasure,EnergyPlusMeasure,UtilityMeasure,ReportingMeasure}

}
Comment on lines +106 to +118
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO


std::vector<std::string> measureLanguageNames;
{
const auto& m = openstudio::MeasureLanguage::getNames();
std::transform(m.cbegin(), m.cend(), back_inserter(measureLanguageNames), [](const auto& x) { return x.second; });
// fmt::print("measureLanguageNames={}\n", measureLanguageNames);
[[maybe_unused]] auto* measureLanguageOpt = newMeasureSubCommand
->add_option("-l,--language", opt->newMeasureOpts.measureLanguage,
fmt::format("Language of Measure [Default: '{}']: {}",
opt->newMeasureOpts.measureLanguage.valueName(), measureLanguageNames))
->option_text("measureLanguage")
->check(CLI::IsMember(measureLanguageNames));
}

newMeasureSubCommand->add_option("-c,--class-name", opt->newMeasureOpts.className, "Measure Class Name [Required]")
->option_text("className")
->required(true);

newMeasureSubCommand->add_option("-n,--name", opt->newMeasureOpts.name, "Measure Human Readable Name [Default: className]")
->option_text("name")
->required(false);

std::vector<std::string> taxonomyTags;
std::vector<std::string> firstLevelTaxonomyTerms = openstudio::BCLMeasure::suggestedFirstLevelTaxonomyTerms();
taxonomyTags.reserve(35); // 2023-05-03: at this moment, this is the total number
for (auto& firstLevelTaxonomyTerm : firstLevelTaxonomyTerms) {
auto secondLevelTaxonomyTerms = openstudio::BCLMeasure::suggestedSecondLevelTaxonomyTerms(firstLevelTaxonomyTerm);
taxonomyTags.reserve(taxonomyTags.size() + secondLevelTaxonomyTerms.size());
std::transform(secondLevelTaxonomyTerms.begin(), secondLevelTaxonomyTerms.end(), std::back_inserter(taxonomyTags),
[&firstLevelTaxonomyTerm](auto&& secondLevelTaxonomyTerm) {
return fmt::format("{}.{}", firstLevelTaxonomyTerm, secondLevelTaxonomyTerm);
});
}

newMeasureSubCommand
->add_option("--taxonomy-tag", opt->newMeasureOpts.taxonomyTag, fmt::format("Taxonomy Tag [Default: '{}']", opt->newMeasureOpts.taxonomyTag))
->option_text("tag")
->required(false)
->check(CLI::IsMember(taxonomyTags))
->capture_default_str();
newMeasureSubCommand->add_option("-d,--description", opt->newMeasureOpts.description, "Description")->option_text("desc")->required(false);
newMeasureSubCommand->add_option("-m,--modeler-description", opt->newMeasureOpts.modelerDescription, "Modeler Description")
->option_text("modeler_desc")
->required(false);

[[maybe_unused]] auto* directoryPathOpt =
newMeasureSubCommand->add_option("DIRECTORY", opt->newMeasureOpts.directoryPath, "Directory for the measure")
->option_text(" ")
->required(true)
->check(CLI::NonexistentPath);
Comment on lines +164 to +168
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw if the target directory exists. BCLMeasure's ctor to create a new one would throw anyways, but this throws earlier and a lot more clearly.

$ mkdir ./test_measure && openstudio labs measure new --class-name MyExamplePythonMeasure ./test_measure
DIRECTORY: Path already exists: ./test_measure


{
static constexpr auto helpOptionsGroupName = "Help";
static constexpr auto newMeasureExamples = R"(Examples:
1. Create a Ruby ModelMeasure:
```
$ openstudio labs measure new --class-name MyExampleRubyModelMeasure
$ openstudio labs measure new --class-name MyExampleRubyModelMeasure --type ModelMeasure --language Ruby
```
2. Pass all optional args to create a Python EnergyPlusMeasure:
```
Comment on lines +170 to +183
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpers, because I feel like this is going to be more than helpful given the number of options we can optionally pass

$ openstudio labs measure new --class-name MyExamplePythonMeasure --type EnergyPlusMeasure --language Python --name "My Python Measure" --description "This is my measure" --modeler-description "This does complicated stuff" --taxonomy-tag "Envelope.Form" ./test_measure
$ openstudio labs measure new -c MyExamplePythonMeasure -t EnergyPlusMeasure -l Python -n "My Python Measure" -d "This is my measure" -m "This does complicated stuff" --taxonomy-tag "Envelope.Form" ./test_measure
```
3. List taxonomy tags
```
$ openstudio labs measure new --list-taxonomy-tags
$ openstudio labs measure new --list-for-first-taxonomy-tag HVAC
```
)";
newMeasureSubCommand->set_help_flag("-h,--help", "Print this help message and exit")->group(helpOptionsGroupName);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'm making a specific "Help" group, might as well make the CLI11 default '--help' fall into that group.


newMeasureSubCommand
->add_flag_callback(
"--examples",
[]() {
fmt::print("{}\n", newMeasureExamples);
std::exit(0); // NOLINT(concurrency-mt-unsafe)
},
"Show Example usage")
->group(helpOptionsGroupName);

[[maybe_unused]] auto* listTaxonomyFlag = newMeasureSubCommand
->add_flag_callback(
"--list-taxonomy-tags",
[taxonomyTags]() {
fmt::print("{}\n", taxonomyTags);
std::exit(0); // NOLINT(concurrency-mt-unsafe)
},
"List all available Taxonomy tags")
->group(helpOptionsGroupName);
Comment on lines +207 to +215
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of flags for taxonomy lookup, because even though I could just try to pass openstudio measure new --taxonomy-tag Badval ./test_measure and look at the error message to get accepted values, this is clearer to use openstudio measure new --list-taxonomy-tags

Also added one that looks up from a first level tag: openstudio measure new --list-for-first-taxonomy-tag Envelope (and throws if that first tag isn't valid)

NOTE: maybe this should live onto the measure subcommand and not the measure new subsubcommand, but that's maybe nitpicking, and it would reduce the readability of the help message in measure subcomand


newMeasureSubCommand
->add_option_function<std::string>(
"--list-for-first-taxonomy-tag",
[taxonomyTags](const std::string& firstLevelTaxonomyTerm) {
auto secondLevelTaxonomyTerms = openstudio::BCLMeasure::suggestedSecondLevelTaxonomyTerms(firstLevelTaxonomyTerm);
std::vector<std::string> taxonomyTags;
taxonomyTags.reserve(secondLevelTaxonomyTerms.size());
std::transform(secondLevelTaxonomyTerms.begin(), secondLevelTaxonomyTerms.end(), std::back_inserter(taxonomyTags),
[&firstLevelTaxonomyTerm](auto&& secondLevelTaxonomyTerm) {
return fmt::format("{}.{}", firstLevelTaxonomyTerm, secondLevelTaxonomyTerm);
});
fmt::print("{}\n", taxonomyTags);
std::exit(0); // NOLINT(concurrency-mt-unsafe)
},
"Limit taxonomy tags to this first level")
->option_text("tag")
// ->excludes(listTaxonomyFlag)
->check(CLI::IsMember(firstLevelTaxonomyTerms))
->group(helpOptionsGroupName);

// newMeasureSubCommand->footer(std::string{newMeasureExamples});
}

newMeasureSubCommand->callback([opt] {
// opt->newMeasureOpts.debug_print();

if (opt->newMeasureOpts.name.empty()) {
opt->newMeasureOpts.name = opt->newMeasureOpts.className;
}
Comment on lines +243 to +245
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the "--name" isn't passed, default to the required --class-name one


BCLMeasure b(opt->newMeasureOpts.name, opt->newMeasureOpts.className, opt->newMeasureOpts.directoryPath, opt->newMeasureOpts.taxonomyTag,
opt->newMeasureOpts.measureType, opt->newMeasureOpts.description, opt->newMeasureOpts.modelerDescription,
opt->newMeasureOpts.measureLanguage);

fmt::print("Created a {} {} with class name '{}' in '{}'\n", opt->newMeasureOpts.measureLanguage.valueName(),
opt->newMeasureOpts.measureType.valueName(), opt->newMeasureOpts.className,
openstudio::toString(openstudio::filesystem::canonical(b.directory())));

std::exit(0); // NOLINT(concurrency-mt-unsafe)
});
}

{

auto* copyMeasureSubCommand = measureCommand->add_subcommand("copy", "Copy a measure");
[[maybe_unused]] auto* existingDirectoryPathOpt =
copyMeasureSubCommand->add_option("EXISTING_DIRECTORY", opt->directoryPath, "Existing Directory for the measure")
->option_text(" ")
->required(true)
->check(CLI::ExistingDirectory);
[[maybe_unused]] auto* newDirectoryPathOpt =
copyMeasureSubCommand->add_option("NEW_DIRECTORY", opt->newMeasureOpts.directoryPath, "New Directory for the measure")
->option_text(" ")
->required(true)
->check(CLI::NonexistentPath);

copyMeasureSubCommand->callback([opt] {
boost::optional<BCLMeasure> b_;
try {
b_ = BCLMeasure(opt->directoryPath);
} catch (...) {
fmt::print(stderr, "Could not find a valid measure at '{}'\n", openstudio::toString(opt->directoryPath));
std::exit(1);
}
auto bClone_ = b_->clone(opt->newMeasureOpts.directoryPath);
if (bClone_) {
// Force updating the UID
bClone_->changeUID();
bClone_->checkForUpdatesFiles();
bClone_->checkForUpdatesXML();
bClone_->save();
fmt::print("Cloned the {} {} with class name '{}' from '{}' to '{}'\n", b_->measureLanguage().valueName(), b_->measureType().valueName(),
b_->className(), openstudio::toString(openstudio::filesystem::canonical(b_->directory())),
openstudio::toString(openstudio::filesystem::canonical(bClone_->directory())));
std::exit(0); // NOLINT(concurrency-mt-unsafe)
} else {
fmt::print(stderr, "Something went wrong when cloning the measure");
std::exit(1); // NOLINT(concurrency-mt-unsafe)
}
Comment on lines +273 to +295
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying a measure. Exit gracefully if the directory isn't a valid measure.

Make sure we do end up with a new UID and everything is up to date in the measure.xml.

});
}

measureCommand->callback([opt, &rubyEngine, &pythonEngine] { MeasureUpdateOptions::execute(*opt, rubyEngine, pythonEngine); });
}

Expand Down Expand Up @@ -370,7 +575,7 @@ print(f"{{measure_name}}, {{measure_typeinfo}}, {{measure_type}}")
}

void MeasureUpdateOptions::execute(MeasureUpdateOptions const& opt, ScriptEngineInstance& rubyEngine, ScriptEngineInstance& pythonEngine) {
opt.debug_print();
// opt.debug_print();

if (opt.server_port > 0) {
const auto measureManagerCmd = fmt::format(
Expand Down Expand Up @@ -463,5 +668,17 @@ end
fmt::print("\n\n");
}

void MeasureNewOptions::debug_print() const {
fmt::print("\nMeasureNewOptions:\n");
fmt::print("name={}\n", name);
fmt::print("className={}\n", className);
fmt::print("directoryPath={}\n", openstudio::toString(directoryPath));
fmt::print("taxonomyTag={}\n", taxonomyTag);
fmt::print("measureType={}\n", measureType.valueName());
fmt::print("description={}\n", description);
fmt::print("modelerDescription={}\n", modelerDescription);
fmt::print("measureLanguage={}\n", measureLanguage.valueName());
};

} // namespace cli
} // namespace openstudio
18 changes: 18 additions & 0 deletions src/cli/MeasureUpdateCommand.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,30 @@
#define CLI_MEASUREUPDATECOMMAND_HPP

#include <CLI/App.hpp>

#include "../utilities/core/Filesystem.hpp"
#include "../utilities/bcl/BCLEnums.hpp"

namespace openstudio {

class ScriptEngineInstance;

namespace cli {

struct MeasureNewOptions
{
std::string name;
std::string className = "MyExampleMeasure";
openstudio::path directoryPath;
std::string taxonomyTag = "Envelope.Fenestration";
openstudio::MeasureType measureType = openstudio::MeasureType::ModelMeasure;
openstudio::MeasureLanguage measureLanguage = openstudio::MeasureLanguage::Ruby;
std::string description = "DESCRIPTION_TEXT";
std::string modelerDescription = "MODELER_DESCRIPTION_TEXT";

void debug_print() const;
};
Comment on lines +20 to +32
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Struct (and its defaults) for new/copy is here


struct MeasureUpdateOptions
{
public:
Expand All @@ -33,6 +49,8 @@ namespace cli {
bool run_tests = false;

unsigned server_port = 0;

MeasureNewOptions newMeasureOpts;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added to here. because we use make_shared for scoping issues... don't worry about it

};

} // namespace cli
Expand Down