From ce31d968b301796c411d9dd0f3b4846d440fb6f9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 2 Oct 2021 23:34:37 +1000 Subject: [PATCH] Refactoring for APIFormField class - Class now stores field definitions as returned from the server - Complex lookup for multi-level form structures - Improved / increased error handling --- lib/api_form.dart | 252 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 185 insertions(+), 67 deletions(-) diff --git a/lib/api_form.dart b/lib/api_form.dart index 35d82dbe..bb3b147a 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -38,53 +38,125 @@ class APIFormField { // JSON data which defines the field final Map data; + // JSON field definition provided by the server + Map definition = {}; + dynamic initial_data; + // Return the "lookup path" for this field, within the server data + String get lookupPath { + + // Simple top-level case + if (parent.isEmpty && !nested) { + return name; + } + + List path = []; + + if (parent.isNotEmpty) { + path.add(parent); + path.add("child"); + } + + if (nested) { + path.add("children"); + path.add(name); + } + + return path.join("."); + } + + /* + * Extract a field parameter from the provided field definition. + * + * - First the user-provided data is checked + * - Second, the server-provided definition is checked + * - Third, return null + */ + dynamic getParameter(String key) { + + if (data.containsKey(key)) { + return data[key]; + } else if (definition.containsKey(key)) { + return definition[key]; + } else { + return null; + } + } + // Get the "api_url" associated with a related field - String get api_url => (data["api_url"] ?? "") as String; + String get api_url => (getParameter("api_url") ?? "") as String; // Get the "model" associated with a related field - String get model => (data["model"] ?? "") as String; + String get model => (getParameter("model") ?? "") as String; // Is this field hidden? - bool get hidden => (data["hidden"] ?? false) as bool; + bool get hidden => (getParameter("hidden") ?? false) as bool; + + // Is this field nested? (Nested means part of an array) + // Note: This parameter is only defined locally + bool get nested => (data["nested"] ?? false) as bool; + + // What is the "parent" field of this field? + // Note: This parameter is only defined locally + String get parent => (data["parent"] ?? "") as String; // Is this field read only? - bool get readOnly => (data["read_only"] ?? false) as bool; + bool get readOnly => (getParameter("read_only") ?? false) as bool; - bool get multiline => (data["multiline"] ?? false) as bool; + bool get multiline => (getParameter("multiline") ?? false) as bool; // Get the "value" as a string (look for "default" if not available) - dynamic get value => data["value"] ?? data["default"]; + dynamic get value => getParameter("value") ?? data["default"]; // Get the "default" as a string - dynamic get defaultValue => data["default"]; + dynamic get defaultValue => getParameter("default"); + // Construct a set of "filters" for this field (e.g. related field) Map get filters { Map _filters = {}; - // Start with the provided "model" filters - if (data.containsKey("filters")) { + // Start with the field "definition" (provided by the server) + if (definition.containsKey("filters")) { - dynamic f = data["filters"]; + try { + var fDef = definition["filters"] as Map; - if (f is Map) { - f.forEach((key, value) { - _filters[key as String] = value.toString(); + fDef.forEach((String key, dynamic value) { + _filters[key] = value.toString(); }); + + } catch (error) { + // pass } } - // Now, look at the provided "instance_filters" - if (data.containsKey("instance_filters")) { + // Next, look at any "instance_filters" provided by the server + if (definition.containsKey("instance_filters")) { - dynamic f = data["instance_filters"]; + try { + var fIns = definition["instance_filters"] as Map; - if (f is Map) { - f.forEach((key, value) { - _filters[key as String] = value.toString(); + fIns.forEach((String key, dynamic value) { + _filters[key] = value.toString(); }); + } catch (error) { + // pass + } + + } + + // Finally, augment or override with any filters provided by the calling function + if (data.containsKey("filters")) { + try { + var fDat = data["filters"] as Map; + + fDat.forEach((String key, dynamic value) { + _filters[key] = value.toString(); + }); + } catch (error) { + // pass } } @@ -108,17 +180,17 @@ class APIFormField { } // Is this field required? - bool get required => (data["required"] ?? false) as bool; + bool get required => (getParameter("required") ?? false) as bool; - String get type => (data["type"] ?? "").toString(); + String get type => (getParameter("type") ?? "").toString(); - String get label => (data["label"] ?? "").toString(); + String get label => (getParameter("label") ?? "").toString(); - String get helpText => (data["help_text"] ?? "").toString(); + String get helpText => (getParameter("help_text") ?? "").toString(); - String get placeholderText => (data["placeholder"] ?? "").toString(); + String get placeholderText => (getParameter("placeholder") ?? "").toString(); - List get choices => (data["choices"] ?? []) as List; + List get choices => (getParameter("choices") ?? []) as List; Future loadInitialData() async { @@ -547,6 +619,72 @@ Map extractFields(APIResponse response) { return result as Map; } +/* + * Extract a field definition (map) from the provided JSON data. + * + * Notes: + * - If the field is a top-level item, the provided "path" may be a simple string (e.g. "quantity"), + * - If the field is buried in the JSON data, the "path" may use a dotted notation e.g. "items.child.children.quantity" + * + * The map "tree" is traversed based on the provided lookup string, which can use dotted notation. + * This allows complex paths to be used to lookup field information. + */ +Map extractFieldDefinition(Map data, String lookup) { + + List path = lookup.split("."); + + // Shadow copy the data for path traversal + Map _data = data; + + // Iterate through all but the last element of the path + for (int ii = 0; ii < (path.length - 1); ii++) { + + String el = path[ii]; + + if (!_data.containsKey(el)) { + print("Could not find field definition for ${lookup}:"); + print("- Key ${el} missing at index ${ii}"); + return {}; + } + + try { + _data = _data[el] as Map; + } catch (error, stackTrace) { + print("Could not find sub-field element '${el}' for ${lookup}:"); + print(error.toString()); + + // Report the error + sentryReportError(error, stackTrace); + return {}; + } + } + + String el = path.last; + + if (!_data.containsKey(el)) { + print("Could not find field definition for ${lookup}"); + print("- Final field path ${el} missing from data"); + return {}; + } else { + + try { + Map definition = _data[el] as Map; + + return definition; + } catch (error, stacktrace) { + print("Could not find field definition for ${lookup}"); + print(error.toString()); + + // Report the error + sentryReportError(error, stacktrace); + + return {}; + } + + } +} + + /* * Launch an API-driven form, * which uses the OPTIONS metadata (at the provided URL) @@ -577,9 +715,10 @@ Future launchApiForm( return; } - var availableFields = extractFields(options); + // List of fields defined by the server + Map serverFields = extractFields(options); - if (availableFields.isEmpty) { + if (serverFields.isEmpty) { // User does not have permission to perform this action showSnackIcon( L10().response403, @@ -592,53 +731,32 @@ Future launchApiForm( // Construct a list of APIFormField objects List formFields = []; - // Iterate through the provided fields we wish to display + APIFormField field; + for (String fieldName in fields.keys) { - // Check that the field is actually available at the API endpoint - if (!availableFields.containsKey(fieldName)) { - print("Field '${fieldName}' not available at '${url}'"); + dynamic data = fields[fieldName]; - sentryReportMessage( - "API form called with unknown field '${fieldName}'", - context: { - "url": url.toString(), - } - ); + Map fieldData = {}; + if (data is Map) { + fieldData = Map.from(data); + } + + // Iterate through the provided fields we wish to display + + field = APIFormField(fieldName, fieldData); + + // Extract the definition of this field from the data received from the server + field.definition = extractFieldDefinition(serverFields, field.lookupPath); + + // Skip fields with empty definitions + if (field.definition.isEmpty) { + print("ERROR: Empty field definition for field '${fieldName}'"); continue; } - final remoteField = Map.from(availableFields[fieldName] as Map); - final localField = Map.from(fields[fieldName] as Map); - - // Override defined field parameters, if provided - for (String key in localField.keys) { - // Special consideration must be taken here! - if (key == "filters") { - - if (!remoteField.containsKey("filters")) { - remoteField["filters"] = {}; - } - - var filters = localField["filters"]; - - if (filters is Map) { - filters.forEach((key, value) { - remoteField["filters"][key] = value; - }); - } - - } else { - remoteField[key] = localField[key]; - } - } - - if (modelData.containsKey(fieldName)) { - remoteField["value"] = modelData[fieldName]; - } - - formFields.add(APIFormField(fieldName, remoteField)); + formFields.add(field); } // Grab existing data for each form field