vstakhov / libucl

Universal configuration library parser
BSD 2-Clause "Simplified" License
1.62k stars 140 forks source link

How to use macros? #200

Closed no-more-secrets closed 5 years ago

no-more-secrets commented 5 years ago

It is not clear from the documentation or examples how to use macros or how they work:

1) Can a user define a macro in a UCL file? 2) How do macros take arguments? Sometimes there are keyword arguments passed in parenthesis, and then there is a string after the macro name. What is the syntax for macros? 3) How do they work?

more info please, Thanks

MeachamusPrime commented 5 years ago

It's a very powerful, but somewhat convoluted process to use a macro, with all due respect to @vstakhov for even offering the feature. However, that may just be my primarily C++ background and experience talking. You can totally fail when using macros if you don't have a passing understanding of how libucl works internally. Here's some messy C++ example code to get you started:

Compile and run:

$ g++ -g -I../libucl/include/ -L../libucl/build/ -lucl parser.cc -o parser
$ ./parser test.ucl

Failing input ucl file (wrong version):

.version 2

foo .uuid "abcdef01-2345-6789-abcd-ef0123456789" {
  thing1 = true;
  thing2 = 4;
  thing3 = "blue";
}

bar {
  .uuid "98765432-10fe-dcba-9876-543210fedcba";
  fish1 = "red";
  fish2 = "blue";
}

Successful input ucl file:

.version 1

foo .uuid "abcdef01-2345-6789-abcd-ef0123456789" {
  thing1 = true;
  thing2 = 4;
  thing3 = "blue";
}

bar {
  .uuid "98765432-10fe-dcba-9876-543210fedcba";
  fish1 = "red";
  fish2 = "blue";
}

Successful output:

Resulting JSON Root Dump:
{
    "version": 1,
    "foo": {
        "uuid": "abcdef01-2345-6789-abcd-ef0123456789",
        "thing1": true,
        "thing2": 4,
        "thing3": "blue"
    },
    "bar": {
        "uuid": "98765432-10fe-dcba-9876-543210fedcba",
        "fish1": "red",
        "fish2": "blue"
    }
}

Unpack:
version = 1;
foo {
    uuid = "abcdef01-2345-6789-abcd-ef0123456789";
    thing1 = true;
    thing2 = 4;
    thing3 = "blue";
}
bar {
    uuid = "98765432-10fe-dcba-9876-543210fedcba";
    fish1 = "red";
    fish2 = "blue";
}

If you end up with macros that can be nested, I'd recommend storing the macro name and line number in a stack while calling ucl_parser_insert_chunk so you can unroll the macros if you error out for some reason.

#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdio.h>
#include <string>
#include <vector>

#include <ucl++.h>

// #define WINDOWS
#ifdef WINDOWS
#include <direct.h>
#define GetCurrentDir _getcwd
#else
#include <unistd.h>
#define GetCurrentDir getcwd
#endif

#define UUID_LENGTH 36
#define VERSION_KEY "version"
#define UUID_KEY "uuid"

// Current document Version
const constexpr int kCurrentVersion = 1;

void TrimWhitespace(std::string &str, bool newlines = true) {
  if (str.empty()) return;
  std::string whitespace;
  if (newlines)
    whitespace = " \t\n";
  else
    whitespace = " \t";
  str.erase(str.find_last_not_of(whitespace) + 1);
  str.erase(0, str.find_first_not_of(whitespace));
}

std::string GetCurrentDirectory( void ) {
  char buff[FILENAME_MAX];
  GetCurrentDir( buff, FILENAME_MAX );
  std::string current_working_dir(buff);
  return current_working_dir;
}

void ConsumeUclObject(struct ucl_parser *parser, std::string &result,
                      uint64_t initial_depth = 1) {
  uint64_t depth = initial_depth;
  unsigned char next = ucl_parser_chunk_peek(parser);

  if (next == '{')
    depth++;
  else if (next == '}')
    depth--;

  while (depth > 0 && next != 0) {
    result += *reinterpret_cast<char *>(&next);
    ucl_parser_chunk_skip(parser);
    next = ucl_parser_chunk_peek(parser);

    if (next == '{')
      depth++;
    else if (next == '}')
      depth--;
  }
}

void uuid_handler_format_error(std::ostream &error) {
  error << "Required format was not parsed correctly after ." << UUID_KEY
        << " macro.\n"
        << "\t." << UUID_KEY << " <36 character UUID value>;\n"
        << "\t<foo/bar> ." << UUID_KEY
        << " <36 character UUID value> {" << std::endl;
}

void version_handler_format_error(std::ostream &error) {
  error << "Required format was not parsed correctly after ." << VERSION_KEY
        << " macro.\n"
        << "\t." << VERSION_KEY << " <version number>;\n"
        << std::endl;
}

class Parser {
  bool bError_;

  static bool uuid_handler(const unsigned char *data, size_t len,
                           const ucl_object_t *args, void *ud) {
    ucl::Ucl::macro_userdata_s *userdata =
        reinterpret_cast<ucl::Ucl::macro_userdata_s *>(ud);
    if (userdata == nullptr) return false;

    struct ucl_parser *parser = userdata->parser;
    if (parser == nullptr) return false;

    Parser *detail_parser = reinterpret_cast<Parser *>(userdata->userdata);
    if (detail_parser == nullptr) return false;

    std::string uuid(reinterpret_cast<const char *>(data), len);

    bool open_brace = (uuid.find_last_of("{") != std::string::npos);
    size_t closer = uuid.find_last_of("{;");
    if (closer != std::string::npos) {
      uuid = uuid.substr(0, closer);
    }

    TrimWhitespace(uuid);

    if (uuid.length() != UUID_LENGTH) {
      detail_parser->bError_ = true;
      uuid_handler_format_error(std::cerr);
      return false;
    }

    // Add Chunks
    std::string chunks;
    chunks += UUID_KEY " = \"" + uuid.substr(0, UUID_LENGTH) + "\";\n";

    if (ucl_parser_chunk_peek(parser) == '{' || open_brace) {
      if (!open_brace) ucl_parser_chunk_skip(parser);
      ConsumeUclObject(parser, chunks);
    }

    bool chunk_success = ucl_parser_insert_chunk(
        parser, reinterpret_cast<const unsigned char *>(chunks.c_str()),
        chunks.length());
    if (!chunk_success || ucl_parser_get_error_code(parser) != 0) {
      detail_parser->bError_ = true;
      uuid_handler_format_error(std::cerr);
      return false;
    }

    return true;
  }

  static bool version_handler(const unsigned char *data, size_t len,
                              const ucl_object_t *args, void *ud) {
    ucl::Ucl::macro_userdata_s *userdata =
        reinterpret_cast<ucl::Ucl::macro_userdata_s *>(ud);
    if (userdata == nullptr) return false;

    struct ucl_parser *parser = userdata->parser;
    if (parser == nullptr) return false;

    Parser *detail_parser = reinterpret_cast<Parser *>(userdata->userdata);
    if (detail_parser == nullptr) return false;

    std::string version(reinterpret_cast<const char *>(data), len);

    if (version.find_last_of("{") != std::string::npos) {
      detail_parser->bError_ = true;
      version_handler_format_error(std::cerr);
      return false;
    }

    size_t closer = version.find_last_of(";");
    if (closer != std::string::npos) {
      version = version.substr(0, closer);
    }

    TrimWhitespace(version);

    if (stol(version) != kCurrentVersion) {
      detail_parser->bError_ = true;
      version_handler_format_error(std::cerr);
      return false;
    }

    // Add Chunks
    std::string chunks;
    chunks += VERSION_KEY " = " + version + ";\n";

    bool chunk_success = ucl_parser_insert_chunk(
        parser, reinterpret_cast<const unsigned char *>(chunks.c_str()),
        chunks.length());
    if (!chunk_success || ucl_parser_get_error_code(parser) != 0) {
      detail_parser->bError_ = true;
      version_handler_format_error(std::cerr);
      return false;
    }

    return true;
  }

public:
  int
  parse(std::istream &stream, const std::string &filename) {
    std::cout << "Parser::parse() filename = " << filename << std::endl;

    // Add macro handlers to parser
    ucl::Ucl::macro_handler_s handler;
    std::vector<std::tuple<std::string /*name*/, ucl::Ucl::macro_handler_s,
                           void * /*userdata*/> >
        macros;
    handler.handler = uuid_handler;
    macros.push_back(std::make_tuple(std::string(UUID_KEY), handler, this));
    handler.handler = version_handler;
    macros.push_back(std::make_tuple(std::string(VERSION_KEY), handler, this));

    // Parse ucl data
    std::string chunks(std::istreambuf_iterator<char>(stream), {});
    std::string error;
    bError_ = false;

    ucl::Ucl uclRoot =
        ucl::Ucl::parse(chunks, macros, error, UCL_DUPLICATE_MERGE);
    if (!error.empty()) {
      std::cerr << "Parse error:\n" << error << std::endl;
      return 1;
    }

    if (bError_) return 1;

    std::cout << "Resulting JSON Root Dump:\n"
              << uclRoot.dump(UCL_EMIT_JSON) << std::endl;

    std::cout << "\nUnpack:\n"
              << uclRoot.dump(UCL_EMIT_CONFIG) << std::endl;

    // TODO: Use your uclRoot here
    return 0;
  }
};

int main(int argc, char *argv[]) {
  if (argc != 2) {
    std::cerr << "usage: parser inputfile\n";
    return 1;
  }

  std::string infile = argv[1];

  std::ifstream input;
  input.open(infile);

  if (!input) {
    std::cerr << "Failed to open requested input file: '" << infile << "'!" << std::endl;
    return 1;
  }

  return Parser().parse(input, infile);
}
no-more-secrets commented 5 years ago

Thanks for that; I guess what I'm looking for is just like a 2-3 (english) sentences explaining what a UCL macro is and what they do. For example, are they the kind of macros that I can define and use within the UCL file, or does defining a macro require adding C/C++ code to augment the parser? If the latter then I doubt I would use them.

vstakhov commented 5 years ago

Second option.

no-more-secrets commented 5 years ago

Thank you