Skip to content

Commit e8d9eee

Browse files
Adding github action to validate contextual survey json (#104)
* Adding schema + yml for workflow * Use action with dart file instead * Using dart code to validate json instead * Delete contextual-survey-schema.json * Formatting * Set upper bound for sampling rate * Update contextual-json-validator.yml * Revert "Update contextual-json-validator.yml" This reverts commit e93cf9e. * Update contextual-json-validator.yml * Update contextual-json-validator.yml * Apply suggestions from code review Co-authored-by: Parker Lougheed <parlough@gmail.com> * Apply analysis fixes * Variable for `surveyObject` keys * Apply suggestions from code review * Adding new analyze job --------- Co-authored-by: Parker Lougheed <parlough@gmail.com>
1 parent a4e5fc1 commit e8d9eee

File tree

3 files changed

+204
-2
lines changed

3 files changed

+204
-2
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: contextual-json-validator
2+
on:
3+
push:
4+
branches: [ main, master ]
5+
pull_request:
6+
branches: [ main, master ]
7+
workflow_dispatch:
8+
schedule:
9+
- cron: '0 0 * * 0' # weekly
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
json-yaml-validate:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
19+
- uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f
20+
21+
- run: dart surveys/validator/json-validator.dart
22+
23+
analyze:
24+
runs-on: ubuntu-latest
25+
defaults:
26+
run:
27+
working-directory: surveys/validator
28+
steps:
29+
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
30+
- uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f
31+
32+
- name: Verify formatting
33+
run: dart format --output=none --set-exit-if-changed .
34+
35+
- name: Analyze Dart files
36+
run: dart analyze --fatal-infos

surveys/contextual-survey-metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"startDate": "2023-07-01T09:00:00-07:00",
55
"endDate": "2023-08-31T09:00:00-07:00",
66
"description": "Help improve Flutter's release builds with this 3-question survey!",
7-
"snoozeForMinutes": "7200",
8-
"samplingRate": "0.1",
7+
"snoozeForMinutes": 7200,
8+
"samplingRate": 0.1,
99
"conditions": [
1010
{
1111
"field": "logFileStats.recordCount",

surveys/validator/json-validator.dart

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
void main() {
5+
final contextualSurveyFile = File('surveys/contextual-survey-metadata.json');
6+
final jsonContents = jsonDecode(contextualSurveyFile.readAsStringSync());
7+
8+
if (jsonContents is! List) {
9+
throw ArgumentError('The json file must be a list');
10+
}
11+
12+
for (final surveyObject in jsonContents) {
13+
// Ensure that each list item is a json object / map
14+
if (surveyObject is! Map) {
15+
throw ArgumentError('Each item in the array must be a map');
16+
}
17+
18+
// Ensure that the number of keys found in each object is correct
19+
if (surveyObject.keys.length != requiredKeys.length) {
20+
throw ArgumentError(
21+
'There should only be ${requiredKeys.length} keys per survey object');
22+
}
23+
24+
// Ensure that the keys themselves match what has been defined
25+
final surveyObjectKeySet = surveyObject.keys.toSet();
26+
if (surveyObjectKeySet.intersection(requiredKeys).length !=
27+
requiredKeys.length) {
28+
throw ArgumentError('Missing the following keys: '
29+
'${requiredKeys.difference(surveyObjectKeySet).join(', ')}');
30+
}
31+
32+
final uniqueId = surveyObject['uniqueId'] as String;
33+
final startDate = DateTime.parse(surveyObject['startDate'] as String);
34+
final endDate = DateTime.parse(surveyObject['endDate'] as String);
35+
final description = surveyObject['description'] as String;
36+
final snoozeForMinutes = surveyObject['snoozeForMinutes'] as int;
37+
final samplingRate = surveyObject['samplingRate'] as double;
38+
final conditionList = surveyObject['conditions'] as List;
39+
final buttonList = surveyObject['buttons'] as List;
40+
41+
// Ensure all of the string values are not empty
42+
if (uniqueId.isEmpty) {
43+
throw ArgumentError('Unique ID cannot be an empty string');
44+
}
45+
if (description.isEmpty) {
46+
throw ArgumentError('Description cannot be an empty string');
47+
}
48+
49+
// Validation on the periods
50+
if (startDate.isAfter(endDate)) {
51+
throw ArgumentError('End date is before the start date');
52+
}
53+
54+
// Ensure the numbers are greater than zero and valid
55+
if (snoozeForMinutes == 0) {
56+
throw ArgumentError('Snooze minutes must be greater than 0');
57+
}
58+
if (samplingRate == 0) {
59+
throw ArgumentError('Sampling rate must be between 0 and 1 inclusive');
60+
}
61+
if (samplingRate > 1) {
62+
throw ArgumentError('Sampling rate must be between 0 and 1 inclusive');
63+
}
64+
65+
// Validation on the condition array
66+
for (final conditionObject in conditionList) {
67+
if (conditionObject is! Map) {
68+
throw ArgumentError('Each item in the condition array must '
69+
'be a map for survey: $uniqueId');
70+
}
71+
if (conditionObject.keys.length != conditionRequiredKeys.length) {
72+
throw ArgumentError('Each condition object should only have '
73+
'${conditionRequiredKeys.length} keys');
74+
}
75+
76+
final field = conditionObject['field'] as String;
77+
final operator = conditionObject['operator'] as String;
78+
final value = conditionObject['value'] as int;
79+
80+
if (field.isEmpty) {
81+
throw ArgumentError('Field in survey: $uniqueId must not be empty');
82+
}
83+
if (!allowedConditionOperators.contains(operator)) {
84+
throw ArgumentError(
85+
'Non-valid operator found in condition for survey: $uniqueId');
86+
}
87+
if (value < 0) {
88+
throw ArgumentError('Value for each condition must not be negative');
89+
}
90+
}
91+
92+
// Validation on the button array
93+
for (final buttonObject in buttonList) {
94+
if (buttonObject is! Map) {
95+
throw ArgumentError('Each item in the button array must '
96+
'be a map for survey: $uniqueId');
97+
}
98+
if (buttonObject.keys.length != buttonRequiredKeys.length) {
99+
throw ArgumentError('Each button object should only have '
100+
'${buttonRequiredKeys.length} keys');
101+
}
102+
103+
final buttonText = buttonObject['buttonText'] as String;
104+
final action = buttonObject['action'] as String;
105+
final url = buttonObject['url'] as String?;
106+
// ignore: unused_local_variable
107+
final promptRemainsVisible = buttonObject['promptRemainsVisible'] as bool;
108+
109+
if (buttonText.isEmpty) {
110+
throw ArgumentError(
111+
'Cannot have empty text for a given button in survey: $uniqueId');
112+
}
113+
if (!allowedButtonActions.contains(action)) {
114+
throw ArgumentError('The action: "$action" is not allowed');
115+
}
116+
if (url != null && url.isEmpty) {
117+
throw ArgumentError('URL values must be a non-empty string or "null"');
118+
}
119+
}
120+
}
121+
}
122+
123+
/// The allowed action strings for a given button
124+
const allowedButtonActions = {
125+
'accept',
126+
'dismiss',
127+
'snooze',
128+
};
129+
130+
/// The allowed operators for a given condition item
131+
const allowedConditionOperators = {
132+
'>=',
133+
'<=',
134+
'>',
135+
'<',
136+
'==',
137+
'!=',
138+
};
139+
140+
/// Required keys for the button object
141+
const buttonRequiredKeys = [
142+
'buttonText',
143+
'action',
144+
'url',
145+
'promptRemainsVisible',
146+
];
147+
148+
/// Required keys for the condition object
149+
const conditionRequiredKeys = [
150+
'field',
151+
'operator',
152+
'value',
153+
];
154+
155+
/// The top level keys that must exist for each json object
156+
/// in the array
157+
const requiredKeys = {
158+
'uniqueId',
159+
'startDate',
160+
'endDate',
161+
'description',
162+
'snoozeForMinutes',
163+
'samplingRate',
164+
'conditions',
165+
'buttons',
166+
};

0 commit comments

Comments
 (0)