Skip to content

Instantly share code, notes, and snippets.

@mymikemiller
Created May 24, 2021 15:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mymikemiller/6e4f46675e7ede6291d8cdaf59c91e9d to your computer and use it in GitHub Desktop.
Save mymikemiller/6e4f46675e7ede6291d8cdaf59c91e9d to your computer and use it in GitHub Desktop.
Parses Candid strings returned from the Internet Computer into Records, which are maps of Strings to Strings, Doubles, Vectors or other Records
class CandidValue {}
class EntryValue extends CandidValue {
final String key;
final CandidValue value;
EntryValue(this.key, this.value);
}
class NumberValue extends CandidValue {
final double number;
NumberValue(this.number);
}
class StringValue extends CandidValue {
final String string;
StringValue(this.string);
}
class RecordValue extends CandidValue {
// A map of the record's keys to their values. Values might be simple
// strings, other records or vectors of records.
final Map<String, CandidValue> record;
RecordValue(this.record);
}
class VectorValue extends CandidValue {
final List<RecordValue> vector;
VectorValue(this.vector);
}
class CandidResult<V extends CandidValue> {
final V value;
final String remainingCandid;
CandidResult(this.value, this.remainingCandid);
}
...
static String getString(CandidValue candidValue,
[List<String> path, String defaultValue]) {
String unescape(String str) => str.replaceAll('\\\"', '\"');
if (candidValue is StringValue) {
return candidValue.string;
}
if (candidValue == null) {
throw 'Null CandidValue';
}
if (!(candidValue is RecordValue)) {
throw 'Can only get a string from a StringValue or a RecordValue with a path to a StringValue';
}
final lastKey = path.removeLast();
final terminalRecord = getRecord(candidValue, path);
if (!terminalRecord.containsKey(lastKey)) {
// print('Key not found: $lastKey');
// return defaultValue;
throw 'Key not found: $lastKey';
}
candidValue = terminalRecord[lastKey];
if (candidValue is StringValue) {
return unescape(candidValue.string);
}
throw 'Value is not a StringValue';
}
static double getNumber(CandidValue candidValue,
[List<String> path, double defaultValue]) {
if (candidValue is NumberValue) {
return candidValue.number;
}
if (candidValue == null) {
throw 'Null CandidValue';
}
if (!(candidValue is RecordValue)) {
throw 'Can only get a number from a NumberValue or a RecordValue with a path to a NumberValue';
}
final lastKey = path.removeLast();
final terminalRecord = getRecord(candidValue, path);
if (!terminalRecord.containsKey(lastKey)) {
print('Key not found: $lastKey');
return defaultValue;
// throw 'Key not found: $lastKey';
}
candidValue = terminalRecord[lastKey];
if (candidValue is NumberValue) {
return candidValue.number;
}
throw 'Value is not a NumberValue';
}
static List<RecordValue> getVector(CandidValue candidValue,
[List<String> path]) {
if (candidValue is VectorValue) {
return candidValue.vector;
}
if (!(candidValue is RecordValue)) {
throw 'Can only get a vector from a VectorValue or a RecordValue with a path to a VectorValue';
}
final lastKey = path.removeLast();
final terminalRecord = getRecord(candidValue, path);
candidValue = terminalRecord[lastKey];
if (candidValue is VectorValue) {
return candidValue.vector;
}
throw 'Value is not a VectorValue';
}
static Map<String, CandidValue> getRecord(RecordValue recordValue,
[List<String> path]) {
if (recordValue == null) {
throw 'Cannot get record from a null RecordValue';
}
if (path == null || path.isEmpty) {
return recordValue.record;
}
for (var key in path) {
if (!(recordValue is RecordValue)) {
throw 'Encountered non-record value in path';
}
if (!recordValue.record.containsKey(key)) {
throw 'Record does not contain key "$key"';
}
recordValue = recordValue.record[key];
}
if (recordValue is RecordValue) {
return recordValue.record;
}
throw 'Value is not a RecordValue';
}
static const stringSignifier = '"';
static const recordSignifier = 'record {';
static const vectorSignifier = 'vec {';
static String removeUnnecessaryLeadingCandid(String candid) {
if (candid.trimLeft().startsWith('(')) {
candid = candid.substring(candid.indexOf('(') + 1);
}
if (candid.trimLeft().startsWith('opt ')) {
candid = candid.substring(candid.indexOf('opt ') + 4);
}
return candid;
}
// Recursive function that, given a candid string, returns a result
// representing the records within.
static CandidResult parseCandid(String candid) {
var sanitizedCandid = removeUnnecessaryLeadingCandid(candid);
final firstRecord = sanitizedCandid.indexOf(recordSignifier);
final firstVector = sanitizedCandid.indexOf(vectorSignifier);
final firstEntry = sanitizedCandid.indexOf('=');
final firstString = sanitizedCandid.indexOf(stringSignifier);
final firstNumber = sanitizedCandid.indexOf(RegExp(r'\d'));
// Find the first of those
final first = [
firstRecord,
firstVector,
firstEntry,
firstString,
firstNumber
].where((element) => element >= 0).reduce(min);
if (firstRecord == first) {
// Record
return parseCandidAsRecord(sanitizedCandid);
} else if (firstVector == first) {
// Vector
return parseCandidAsVector(sanitizedCandid);
} else if (firstEntry == first) {
// Entry
return parseCandidAsEntry(sanitizedCandid);
} else if (firstString == first) {
// String
return parseCandidAsString(sanitizedCandid);
} else if (firstNumber == first) {
// Number
return parseCandidAsNumber(sanitizedCandid);
} else {
throw 'Unable to parse line starting with ${sanitizedCandid.substring(20)}';
}
}
static CandidResult<EntryValue> parseCandidAsEntry(String candid) {
final match =
RegExp(r'^\s*(\w+)\s*=\s*(.*$)', dotAll: true).firstMatch(candid);
if (match == null) {
throw ('Candid string does not begin with an entry. Candid starts with: ${candid.substring(0, 50)}');
}
final key = match.group(1);
final rest = match.group(2);
final valueResult =
parseCandid(rest); // should leave end quote in remainingcandid
if (valueResult.value is EntryValue) {
throw ('Invalid: Found an entry as the value of another entry.');
}
return CandidResult(
EntryValue(key, valueResult.value), valueResult.remainingCandid);
}
static CandidResult<StringValue> parseCandidAsString(String candid) {
if (!candid.startsWith('"')) {
throw ('Cannot parse candid as a string when the candid does not start with double quotes. Candid begins with: ${candid.substring(10)}');
}
// Select everything after the first quote and before the next quote,
// (ignoring escaped quotes). Also select the rest (throwing out an
// optional ending space/semicolon)
final match = RegExp(
r'"(.*?)(?<!\\)"\s*[ ;]\s*(.*$)',
dotAll: true,
).firstMatch(candid);
if (match == null) {
throw ('Failed to find string in candid. Must be surrounded by non-escaped double quotes followed by a semicolon. Candid: $candid');
}
final str = match.group(1);
final rest = match.group(2);
return CandidResult(StringValue(str), rest);
}
static CandidResult<NumberValue> parseCandidAsNumber(String candid) {
if (!candid.startsWith(RegExp(r'\d'))) {
throw ('Cannot parse candid as a number when the candid does not start with a number. Candid begins with: ${candid.substring(10)}');
}
// Select everything that is either a digit or an underscore. Also select
// the rest (throwing out a semicolon and any whitespace at the end if
// found) to pass along with the result
final match = RegExp(
r'([\d_]+);?\s*(.*$)',
dotAll: true,
).firstMatch(candid);
if (match == null) {
throw ('Failed to find number in candid. Candid: $candid');
}
final str = match.group(1).replaceAll('_', '');
final rest = match.group(2);
final num = double.parse(str);
return CandidResult(NumberValue(num), rest);
}
static CandidResult<RecordValue> parseCandidAsRecord(String candid) {
if (!candid.trimLeft().startsWith(recordSignifier)) {
throw ('Candid is not a record. Candid begins with: ${candid.substring(0, 50)}');
}
final record = Map<String, dynamic>();
var remainingCandid = candid
.substring(candid.indexOf(recordSignifier) + recordSignifier.length);
while (remainingCandid.trimLeft().isNotEmpty) {
final entryResult = parseCandid(remainingCandid);
if (!(entryResult.value is EntryValue)) {
throw 'Non-entry item found in candid record: $remainingCandid';
}
final entry = entryResult.value as EntryValue;
// Add the entry
record[entry.key] = entry.value;
remainingCandid = entryResult.remainingCandid;
if (remainingCandid.trimLeft().startsWith('}')) {
// This is the end of the record. We need to figure out how much else
// to remove before we return the record, which varies depending on
// whether the record was in a vector or not.
//
// Records in vectors end in ; or };
remainingCandid = removeLeading(remainingCandid, ['}', ';']);
// These occur for single records. We'll have already removed the end
// curly brace from the call above.
remainingCandid = removeLeading(remainingCandid, [',', ')']);
return CandidResult(RecordValue(Map<String, CandidValue>.from(record)),
remainingCandid);
}
}
throw 'Reached end of candid record without encountering closing brace';
}
static CandidResult<VectorValue> parseCandidAsVector(String candid) {
if (!candid.trimLeft().startsWith(vectorSignifier)) {
throw ('Candid is not a vector. Candid begins with: ${candid.substring(10)}');
}
final vector = [];
var remainingCandid = candid
.substring(candid.indexOf(vectorSignifier) + vectorSignifier.length);
while (remainingCandid.trimLeft().isNotEmpty) {
final recordResult = parseCandid(remainingCandid);
if (!(recordResult.value is RecordValue)) {
throw 'Non-record item found in candid vector: $remainingCandid';
}
final recordValue = recordResult.value as RecordValue;
// Add the entry
vector.add(recordValue);
remainingCandid = recordResult.remainingCandid;
if (remainingCandid.trimLeft().startsWith('}')) {
remainingCandid = removeLeading(remainingCandid, ['}', ';']);
return CandidResult(
VectorValue(List<RecordValue>.from(vector)), remainingCandid);
}
}
throw 'Reached end of candid vector without encountering closing bracket';
}
// Removes tokens from the beginning of the string, one at a time in order,
// stopping immediately when one isn't found
static String removeLeading(String str, List<String> tokens) {
tokens.forEach((token) {
if (str.trimLeft().startsWith(token)) {
str = str.substring(str.indexOf(token) + token.length);
} else {
return str;
}
});
return str;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment