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

A proposition to implement any complex rule on the arguments #401

Open
AdelKS opened this issue Apr 3, 2023 · 0 comments
Open

A proposition to implement any complex rule on the arguments #401

AdelKS opened this issue Apr 3, 2023 · 0 comments

Comments

@AdelKS
Copy link

AdelKS commented Apr 3, 2023

After skimming through #73 #44 and #35, where the main issues from the devs seems to be this one

The main reasoning is that usually options are not just mandatory or optional., but something more complicated

I have a suggestion that should please both parties (the developers and the users)

A simple and elegant way to handle "required", "optional" and actually any complex rule is to implement template expressions

Here's a small working example that handles constraints of type "option 'foo' must appear exactly once and option 'bar' exactly twice" :

#include <cxxopts.hpp>
#include <cstddef>
#include <iostream>

namespace cxxopts {
namespace rules {

// =============================================================
class Option
{
public:
  Option(std::string option_name) : name(option_name) {}

  std::string name;
};
// =============================================================

// =============================================================
class Count
{
public:
  Count(Option option) : option(std::move(option)) {}

  inline size_t count(const ParseResult &parsing) const
  {
    return parsing.count(option.name);
  }

  Option option;
};
// =============================================================

// =============================================================
class ExactCount
{
public:
  ExactCount(Option option, size_t target_count)
    : option(std::move(option)), target_count(target_count) {};

  inline bool operator () (const ParseResult& parsing) const
  {
    return option.count(parsing) == target_count;
  }

  inline explicit operator std::string () const
  {
    if (target_count == 1)
      return "option '--" + option.name + "' must be provided exactly once";
    else return "option '--" + option.name + "' must be provided exactly " + std::to_string(target_count) + " times";
  }

protected:
  Option option;
  size_t target_count;
};

ExactCount operator == (Count option_count, size_t target_count)
{
  return ExactCount(std::move(option_count.option), target_count);
}
// =============================================================


// =============================================================
template <class SubExpression1, class SubExpression2>
class AND
{
public:
  AND(SubExpression1 expr1, SubExpression2 expr2)
    : expr1(std::move(expr1)), expr2(std::move(expr2)) {}

  inline bool operator () (const ParseResult& parsing) const
  {
    return expr1(parsing) && expr2(parsing);
  }

  inline explicit operator std::string () const
  {
    return "(" + std::string(expr1) + ") and (" + std::string(expr2) + ")";
  }

protected:
  SubExpression1 expr1;
  SubExpression2 expr2;

};

template <class SubExpression1, class SubExpression2>
auto operator && (SubExpression1 expr1, SubExpression2 expr2)
{
  return AND(std::move(expr1), std::move(expr2));
}
// =============================================================

template <class Rule>
void enforce(const Rule& rule, const ParseResult& parsing)
{
  if (not rule(parsing))
    throw std::runtime_error("rule not respected: " + std::string(rule));
}

}
}

using namespace cxxopts::rules;

int main(int argc, char *argv[])
{
  cxxopts::Options options(
    "hash-id",
    "Advertising-ID hasher");

  options.add_options()
    ("i,input", "input", cxxopts::value<std::string>())
    ("o,output", "output", cxxopts::value<std::string>())
    ("h,help", "Print usage")
    ;

  auto parsing = options.parse(argc, argv);

  auto rule = (Count(Option("input")) == 1) and (Count(Option("output")) == 2);

  cxxopts::rules::enforce(rule, parsing);

  std::cout << parsing["output"].as<std::string>() << std::endl;

  return 0;
}

After implementing the or, not, xor, nand boolean operations, and all the comparison operations >, >=... on the Count class. We get a great deal of flexibility: rules like "this option can only be used this other one is set ..." can be implemented just with this.

Ordering rules e.g. "this option can only be set after this option" can also be implemented in the same way I think.

One needs to think a little bit more to be able to display in a human understandable way what's wrong when the check rules don't evaluate to true, in the code snippet above we just display the whole rule in text. The user can already subdivide the rules that are not related to make it better: in my example above, I could write this instead:

  auto rule1 = (Count(Option("input")) == 1);
  auto rule2 = (Count(Option("output")) == 2);

  cxxopts::rules::enforce({rule1, rule2}, parsing);

Just leaving this here maybe you guys find it interesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant