Valkyrian Labs logo

Fields and updates

How Field<T> tracks state so that writes change exactly what you changed.

Fields and updates

Every attribute of a generated object is a fortiapi::Field<T> with one of four states:

StateMeaningSent by updateSent by add
absentnever read from the device, never setnono
populatedread from the device, untouched sincenoyes
modifiedset by you (assignment or edit())yes, its valueyes, its value
clearedreset by you with clear()yes, the FortiOS defaultyes, the FortiOS default

Reading

Reads never change a field's state:

1auto policy = firewall::Policies::get(12);2if (policy.comments)                          // has a value3    std::cout << *policy.comments << "\n";4std::cout << policy.name.value_or("(unnamed)") << "\n";5bool denies = policy.action == std::string(firewall::Policy::Action::deny);
Check before dereferencing

value() and *field throw std::logic_error when the field has no value; check with has_value() or operator bool first, or use value_or().

Changing

Assignment marks a field modified. For containers and nested objects, edit() returns a mutable reference and marks the field modified:

1policy.comments = "reviewed";2policy.srcaddr.edit().push_back("printer");   // fortiapi::Ref converts from a name3firewall::Policies::update(policy);           // sends comments and srcaddr only

update returns std::nullopt without sending anything when nothing is modified or cleared. After a successful write the object is clean: modified fields become populated, cleared fields become absent.

Clearing and forgetting

clear() resets an attribute to its FortiOS default (an empty string, 0, the default option or an empty table, from the schema). reset() only forgets the local value so nothing is sent for it.

1policy.comments.clear();       // the device's comment becomes ""2policy.schedule.reset();       // not part of the next update

Creating

A new object starts with every field absent. Set what you need; FortiOS fills in its own defaults for the rest:

1firewall::Address host;2host.name = "printer";3host.subnet = "192.168.1.50 255.255.255.255";4firewall::Addresses::add(host);

When the device assigns the key (a firewall policy created with policyid 0 or unset), add stores the assigned key in the object.

Creates are not transactional

When FortiOS rejects an attribute after it created the object, add throws an ApiError and the object still exists on the device. See Partial creates.

Nested objects and tables

  • A nested object (for example dnsfilter::Profile::ftgd_dns) is updated attribute by attribute: profile.ftgd_dns.edit().options = "error-allow" sends only ftgd-dns.options.
  • References to other objects by name (srcintf, srcaddr, service, ...) are std::vector<fortiapi::Ref>; a Ref converts from a string.
Tables are sent whole

A table (for example ftgd_dns.filters or srcintf) is always sent whole, because FortiOS replaces tables. Edit the list you read and send it back.

Read-only attributes

Attributes the schema marks read-only are parsed but never sent, even if you assign them.

Option constants

Every option attribute has constants nested in its type, for example firewall::Policy::Action::accept or dnsfilter::Profile::BlockAction::redirect. They are std::string_view; assign them with std::string(...).

Serializing yourself

fortiapi::to_json(object, Write::update) and Write::create produce the exact request bodies, and fortiapi::from_json<T>(json) builds an object from a device reply. fortiapi::dirty(object) tells whether an update would send anything.