Advanced Types

In addition to Inlet’s primitive types (bool, int, double, string), user-defined types and functions can also be defined as part of an input file.

In this section, we first describe how Individual Structs can be added to our schemas. We then extend it to Arrays and Dictionaries of Structs.

Individual Structs

Defining And Storing

To add a single (i.e., not array) user-defined type to the input file, use the addStruct method of the Inlet or Container classes to add a Container (collection of Fields and sub-Containers) that will represent the fields of the struct.

Consider a simple Lua table that contains only primitive types, whose definition might look like:

car = {
    make = "BestCompany",
    seats = 2,
    horsepower = 200,
    color = "blue"
}

or in YAML, something like:

car:
  make: BestCompany
  seats: 2
  horsepower: 200
  color: blue

Its Inlet schema can be defined as follows:

struct Car
{
  std::string make;
  std::string color;
  int seats;
  int horsepower;

  // A function can be used to define a schema for a particular struct
  // For convenience, it can be implemented as a static method of the struct
  static void defineSchema(inlet::Container& car_schema)
  {
    car_schema.addString("make", "Make of car");
    car_schema.addString("color", "Color of car").defaultValue("red");
    car_schema.addInt("seats", "Number of seats").range(2, 7);
    car_schema.addInt("horsepower", "Amount of horsepower");
  }
};

This would be used by creating an inlet::Container for the Car instance and then defining the struct schema on that subcontainer, e.g.:

  // Create a container off the global container for the car object
  // then define its schema
  auto& car_schema = inlet.addStruct("car", "Vehicle description");
  Car::defineSchema(car_schema);

Note

The definition of a static defineSchema member function is not required, and is just used for convenience. The schema definition for a class or struct could also be implemented as a free function for third-party types, or even in the same place as the sub-container declaration.

Accessing

In order to convert from Inlet’s internal representation to a custom C++ struct, you must provide deserialization logic. This is accomplished by a specialization of the FromInlet<T> functor for your type T, which implements a member function with the signature T operator()(const inlet::Container&). This function should return an instance of type T with its members populated with the corresponding fields in the input file. For the simple Car example whose schema is defined above, the specialization might look like:

template <>
struct FromInlet<Car>
{
  Car operator()(const inlet::Container& input_data)
  {
    Car result;
    result.make = input_data["make"];
    result.color = input_data["color"];
    result.seats = input_data["seats"];
    result.horsepower = input_data["horsepower"];
    return result;
  }
};

In the above snippet, Container::operator[] is used to extract data from Inlet’s internal representation which is automatically converted to the correct member variable types when the function’s return value is constructed. This conversion does not happen automatically for user-defined types. If a Car object as defined above is located at the path “car” within the input file, it can be retrieved as follows:

Car car = inlet["car"].get<Car>();

Individual Variant Structs

When a single input entry may describe one of several user-defined struct types, use a normal addStruct schema with a discriminator field and provide a FromInlet specialization that constructs the selected alternative. This is useful for inputs that are variant-valued but are not arrays or dictionaries.

For example, a single shape table can use a kind field to select the concrete shape:

  shape = {
    kind = "box",
    width = 3.0,
    height = 4.0
  }

The C++ type can store the concrete result in a std::variant while keeping the Inlet-facing type as a user-defined struct:

struct Circle
{
  double radius;
};

struct Box
{
  double width;
  double height;
};

using Shape = std::variant<Circle, Box>;

template <>
struct FromInlet<Circle>
{
  Circle operator()(const axom::inlet::Container& input_data) { return {input_data["radius"]}; }
};

template <>
struct FromInlet<Box>
{
  Box operator()(const axom::inlet::Container& input_data)
  {
    return {input_data["width"], input_data["height"]};
  }
};

template <>
struct FromInlet<Shape>
{
  // Shape is a std::variant, so it cannot be read through Container::get<T>().
  // Use the public Proxy returned by inlet["shape"] to access its fields instead.
  Shape operator()(const axom::inlet::Proxy& input_data)
  {
    const std::string kind = input_data["kind"];
    if(kind == "circle")
    {
      return Circle {input_data["radius"]};
    }
    else if(kind == "box")
    {
      return Box {input_data["width"], input_data["height"]};
    }

    SLIC_ERROR(axom::fmt::format("Unknown shape discriminator '{}'", kind));
    return Box {0.0, 0.0};
  }
};

void defineShapeSchema(axom::inlet::Container& shape)
{
  shape.addString("kind", "Shape variant discriminator").required().validValues({"circle", "box"});
  shape.addDouble("radius", "Circle radius").required(false);
  shape.addDouble("width", "Box width").required(false);
  shape.addDouble("height", "Box height").required(false);

  shape.registerVerifier([](const axom::inlet::Container& input_data) {
    if(!input_data.isUserProvided("kind"))
    {
      return false;
    }

    const std::string kind = input_data["kind"];
    if(kind == "circle")
    {
      return input_data.isUserProvided("radius") && !input_data.isUserProvided("width") &&
        !input_data.isUserProvided("height");
    }
    else if(kind == "box")
    {
      return input_data.isUserProvided("width") && input_data.isUserProvided("height") &&
        !input_data.isUserProvided("radius");
    }

    return false;
  });
}

Define the schema with addStruct and retrieve the result as the wrapper user-defined type:

  auto& shape_schema = inlet.addStruct("shape", "A single user-defined variant");
  defineShapeSchema(shape_schema);
  const Shape shape = FromInlet<Shape> {}(inlet["shape"]);

Arrays and Dictionaries of Structs

Arrays of user-defined types are also supported in Inlet.

Defining And Storing

Consider a collection of cars, described in Lua as:

fleet = {
  {
    make = "Globex Corp",
    seats = 3,
    horsepower = 155,
    color = "green"
  },
  {
    make = "Initech",
    seats = 4,
    horsepower = 370
    -- uses default value for color
  },
  {
    make = "Cyberdyne",
    seats = 1,
    horsepower = 101,
    color = "silver"
  }
}

or in YAML, as:

fleet:
  - make: Globex Corp
    seats: 3
    horsepower: 155
    color: green
  - make: Initech
    seats: 4
    horsepower: 370
    # uses default value for color
  - make: Cyberdyne
    seats: 1
    horsepower: 101
    color: silver

First, use the addStructArray function to create a subcontainer, then define the schema on that container using the same Car::defineSchema used above:

  // Create a fleet of cars with the same Car::defineSchema
  auto& fleet_schema = inlet.addStructArray("fleet", "A collection of cars");
  Car::defineSchema(fleet_schema);

Note

The schema definition logic for a struct is identical between individual instances of structs and arrays of structs. The distinction is made by Container on which the struct schema is defined - specifically, whether it is obtained via addStruct or addStructArray.

Associative arrays are also supported, using string keys or a mixture of string and integer keys. The addStructDictionary function can be used analogously to the addStructArray function for these associative arrays.

Note

Although many of Inlet’s input file languages do not distinguish between a “dictionary” type and a “record” type, Inlet treats them differently for type safety reasons:

Dictionaries use arbitrary strings or integers for their keys, and their values (entries) can only be retreived as a homogeneous type. In other words, dictionaries must map to std::unordered_map<Key, Value> for fixed key and value types.

Structs contain a fixed set of named fields, but these fields can be of any type. As the name suggests, these map to structs in C++.

Accessing

As with the schema definition, the FromInlet specialization for a user-defined type will work for both single instances of the type and arrays of the type.

To retrieve an array of structs as a contiguous array of user-defined type, use std::vector:

auto fleet = base["fleet"].get<std::vector<Car>>();

Some input file languages support non-contiguous array indexing, so you can also retrieve arrays as std::unordered_map<int, T>:

auto fleet = inlet["fleet"].get<std::unordered_map<int, Car>>();

Note

If a non-contiguous array is retrieved as a (contiguous) std::vector, the elements will be ordered by increasing index.

String-keyed dictionaries are implemented as std::unordered_map<std::string, T> and can be retrieved in the same way as the array above. For dictionaries with a mix of string and integer keys, the inlet::VariantKey type can be used, namely, by retrieving a std::unordered_map<inlet::VariantKey, T>.

Variant Struct Collections

Variant struct collections store a collection whose entries can be selected from a finite set of user-defined struct types. They are useful when every entry in a collection shares the same role, but different entries require different fields. For example, a shapes collection might contain both circles and boxes.

Each entry must contain a string discriminator field. The discriminator value selects which struct schema and FromInlet specialization Inlet should use for that entry.

Defining And Storing

Represent the possible entry types with a std::variant. Each alternative in the variant is a normal user-defined type, so it still provides its own FromInlet specialization:

struct Circle
{
  double radius;
};

struct Box
{
  double width;
  double height;
};

using Shape = std::variant<Circle, Box>;

template <>
struct FromInlet<Circle>
{
  Circle operator()(const inlet::Container& input_data) { return {input_data["radius"]}; }
};

template <>
struct FromInlet<Box>
{
  Box operator()(const inlet::Container& input_data)
  {
    return {input_data["width"], input_data["height"]};
  }
};

void defineShapeSchema(inlet::VariantStructCollection<Shape>& shapes)
{
  shapes.addAlternative<Circle>("circle", [](inlet::Container& circle) {
    circle.addDouble("radius").required();
  });
  shapes.addAlternative<Box>("box", [](inlet::Container& box) {
    box.addDouble("width").required();
    box.addDouble("height").required();
  });
}

For array-like input, use addVariantStructArray. The function takes the collection name and the discriminator field name:

  shapes = {
    { kind = "circle", radius = 2.5 },
    { kind = "box", width = 3.0, height = 4.0 }
  }
  auto shapes_schema = inlet.addVariantStructArray<Shape>("shapes", "kind");
  defineShapeSchema(shapes_schema);

For named dictionary input, use addVariantStructDictionary with the same variant type and discriminator field:

  shapes = {
    ["ball"] = { kind = "circle", radius = 2.5 },
    ["block"] = { kind = "box", width = 3.0, height = 4.0 }
  }
  auto shapes_schema = inlet.addVariantStructDictionary<Shape>("shapes", "kind");
  defineShapeSchema(shapes_schema);

Verification fails if an entry omits the discriminator or uses a discriminator value that was not registered as an alternative.

Accessing

Variant struct collections are retrieved as collections of the same std::variant type used when defining the schema. Call verify before retrieval to check that every entry has a known discriminator:

  if(!inlet.verify())
  {
    SLIC_ERROR("Inlet failed to verify against provided schema");
  }

The array input can be retrieved as std::vector<Variant>:

  const std::vector<Shape> shapes = inlet["shapes"].get<std::vector<Shape>>();

The named dictionary input can be retrieved as std::unordered_map<inlet::VariantKey, Variant>:

  const std::unordered_map<inlet::VariantKey, Shape> shapes_by_name =
    inlet["shapes"].get<std::unordered_map<inlet::VariantKey, Shape>>();

Once retrieved, use normal std::variant access patterns, such as std::visit, std::get_if, or std::holds_alternative, to work with the concrete struct stored in each entry:

void printShapes(const std::vector<Shape>& shapes)
{
  for(const Shape& shape : shapes)
  {
    printShape(shape);
  }
}