Make INFO require "Header: Value" strings; make the "info" element in the JSON output a hash instead of an array.
All checks were successful
Remind unit tests / tests (push) Successful in 36s

This commit is contained in:
Dianne Skoll
2025-01-30 16:58:56 -05:00
parent cb0acb3077
commit e832eb868c
8 changed files with 159 additions and 39 deletions

View File

@@ -516,10 +516,28 @@ MOON, etc.)
If any TAG clauses are present, the \fBtags\fR key will be present and consist
of a comma-separated list of tags.
.TP
.B info [\fIarray\fR]
.B info \fR{ \fIhash\fR }
If any INFO clauses are present, the \fBinfo\fR key will be present. Its
value will be an array of info strings, one for each INFO clause and in
the same order as the INFO clauses.
value will be a hash of info key-value pairs. Each key is the header
from an INFO string, \fIconverted to all upper-case\fR. The value is the
valu from the INFO string.
.RS
.PP
For example, the following REM command:
.PP
.nf
REM INFO "Location: Boardroom" INFO "Summary: None" MSG whatever
.fi
.PP
will produce the following \fBinfo\fR hash:
.PP
.nf
"info" : {
"LOCATION" : "Boardroom",
"SUMMARY" : "None"
},
.fi
.RE
.TP
.B time \fIt\fR
If an AT clause was present, this key will contain the time of the AT clause

View File

@@ -1365,11 +1365,20 @@ to each distinct REM command.
.PP
The \fBINFO\fR keyword is similar to \fBTAG\fR but is intended to convey
metadata about an event, such as its location. Back-ends will have their
own rules about the format of the \fIinfo_string\fRs and must ignore
\fIinfo_string\fRs they don't recognize. Note that \fBINFO\fR must
own rules about which \fIinfo_string\fRs they recognize, and must ignore
\fIinfo_string\fRs they don't recognize. Note that \fBINFO\fR must
be followed by a quoted string; you can include newlines in the string
by supplying them as "\\n".
.PP
An INFO string \fImust\fR be of the form "Header: Value". The header
can consist of any printable character, but cannot contain whitespace.
The value can consist of any characters you like. Space may not
appear before the colon, but can appear afterwards; such space is not
considered to be part of the value. If there is more than one INFO
string for a given reminder, there cannot be any duplicate headers.
Case is ignored when determining if a header is a duplicate of an
existing one.
.PP
For example, a hypothetical back-end might let you set the location
and description of a reminder like this:
.PP

View File

@@ -409,6 +409,23 @@ void PrintJSONString(char const *s)
}
}
void PrintJSONStringUC(char const *s)
{
while (*s) {
switch(*s) {
case '\b': printf("\\b"); break;
case '\f': printf("\\f"); break;
case '\n': printf("\\n"); break;
case '\r': printf("\\r"); break;
case '\t': printf("\\t"); break;
case '"': printf("\\\""); break;
case '\\': printf("\\\\"); break;
default: printf("%c", toupper(*s));
}
s++;
}
}
void PrintJSONKeyPairInt(char const *name, int val)
{
printf("\"");
@@ -2354,6 +2371,41 @@ void WriteJSONTimeTrigger(TimeTrig const *tt)
}
}
static void
WriteJSONInfoChain(TrigInfo *ti)
{
printf("\"info\":{");
while (ti) {
/* Skanky... */
char *colon = (char *) strchr(ti->info, ':');
char const *value;
if (!colon) {
/* Should be impossible... */
ti = ti->next;
continue;
}
/* Terminate the string at the colon */
*colon = 0;
value = colon+1;
while(*value && isspace(*value)) {
value++;
}
printf("\"");
PrintJSONStringUC(ti->info);
printf("\":\"");
PrintJSONString(value);
printf("\"");
/* Restore the value of the colon */
*colon = ':';
if (ti->next) {
printf(",");
}
ti = ti->next;
}
printf("},");
}
void WriteJSONTrigger(Trigger const *t, int include_tags, int today)
{
/* wd is an array of days from 0=monday to 6=sunday.
@@ -2446,18 +2498,7 @@ void WriteJSONTrigger(Trigger const *t, int include_tags, int today)
}
if (include_tags) {
if (t->infos) {
TrigInfo *ti = t->infos;
printf("\"info\":[");
while (ti) {
printf("\"");
PrintJSONString(ti->info);
printf("\"");
if (ti->next) {
printf(",");
}
ti = ti->next;
}
printf("],");
WriteJSONInfoChain(t->infos);
}
PrintJSONKeyPairString("tags", DBufValue(&(t->tags)));
}
@@ -2473,18 +2514,7 @@ static void WriteSimpleEntryProtocol2(CalEntry *e, int today)
PrintJSONKeyPairString("passthru", e->passthru);
PrintJSONKeyPairString("tags", DBufValue(&(e->tags)));
if (e->infos) {
TrigInfo *ti = e->infos;
printf("\"info\":[");
while (ti) {
printf("\"");
PrintJSONString(ti->info);
printf("\"");
if (ti->next) {
printf(",");
}
ti = ti->next;
}
printf("],");
WriteJSONInfoChain(e->infos);
}
if (e->duration != NO_TIME) {
PrintJSONKeyPairInt("duration", e->duration);

View File

@@ -280,3 +280,5 @@ TrigInfo *NewTrigInfo(char const *i);
void FreeTrigInfo(TrigInfo *ti);
void FreeTrigInfoChain(TrigInfo *ti);
int AppendTrigInfo(Trigger *t, char const *info);
int TrigInfoHeadersAreTheSame(char const *i1, char const *i2);
int TrigInfoIsValid(char const *info);

View File

@@ -732,8 +732,16 @@ FreeTrigInfoChain(TrigInfo *ti)
int
AppendTrigInfo(Trigger *t, char const *info)
{
TrigInfo *ti = NewTrigInfo(info);
TrigInfo *last = t->infos;
TrigInfo *ti;
TrigInfo *last;
if (!TrigInfoIsValid(info)) {
Eprint("%s", tr("Invalid INFO string: Must be of the form \"Header: Value\""));
return E_PARSE_ERR;
}
ti = NewTrigInfo(info);
last = t->infos;
if (!ti) {
return E_NO_MEM;
}
@@ -741,10 +749,46 @@ AppendTrigInfo(Trigger *t, char const *info)
t->infos = ti;
return OK;
}
if (TrigInfoHeadersAreTheSame(info, last->info)) {
Eprint("%s", tr("Duplicate INFO headers are not permitted"));
FreeTrigInfo(ti);
return E_PARSE_ERR;
}
while (last->next) {
last = last->next;
if (TrigInfoHeadersAreTheSame(info, last->info)) {
Eprint("%s", tr("Duplicate INFO headers are not permitted"));
FreeTrigInfo(ti);
return E_PARSE_ERR;
}
}
last->next = ti;
return OK;
}
int
TrigInfoHeadersAreTheSame(char const *i1, char const *i2)
{
char const *c1 = strchr(i1, ':');
char const *c2 = strchr(i2, ':');
if (!c1 || !c2) return 1;
if (c1 - i1 != c2 - i2) return 0;
if (!strncasecmp(i1, i2, (c1 - i1))) return 1;
return 0;
}
int
TrigInfoIsValid(char const *info)
{
char const *t;
char const *s = strchr(info, ':');
if (!s) return 0;
if (s == info) return 0;
t = info;
while (t < s) {
if (isspace(*t) || iscntrl(*t)) return 0;
t++;
}
return 1;
}

View File

@@ -1,6 +1,6 @@
FSET msgprefix(x) "Priority: " + x + "; Filename: " + filename() + ": "
REM at 23:56 MSG foo
REM PRIORITY 42 at 23:57 INFO "A piece of info" MSG bar
REM PRIORITY 999 at 23:58 INFO "info1" INFO "info2" MSG quux
REM PRIORITY 42 at 23:57 INFO "Info: yuppers" MSG bar
REM PRIORITY 999 at 23:58 INFO "Info2: Nope" INFO "Info3: heh" MSG quux
DO queue2.rem

View File

@@ -650,6 +650,16 @@ EOF
REM Wed INFO "Location: here" INFO "Summary: Nope" MSG Meeting
EOF
# Invalid info strings
../src/remind - 1 Feb 2024 <<'EOF' >> ../tests/test.out 2>&1
REM Thu INFO "Invalid" MSG wookie
REM Fri INFO ": foo" MSG blat
REM Sun INFO "foo bar baz : blork" MSG uua
# Duplicate info string
REM Sat INFO "Location: here" INFO "location: there" MSG blort
EOF
# Languages
for i in ../include/lang/??.rem ; do
../src/remind -r -q "-ii=\"$i\"" ../tests/tstlang.rem 1 Feb 2024 13:34 >> ../tests/test.out 2>&1

View File

@@ -23205,12 +23205,12 @@ Enabling test mode: This is meant for the acceptance test.
Do not use --test in production.
In test mode, the system time is fixed at 2025-01-06@19:00
NOTE JSONQUEUE
[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":["info1","info2"],"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":["A piece of info"],"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}]
[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":{"INFO2":"Nope","INFO3":"heh"},"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":{"INFO":"yuppers"},"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}]
NOTE ENDJSONQUEUE
Enabling test mode: This is meant for the acceptance test.
Do not use --test in production.
In test mode, the system time is fixed at 2025-01-06@19:00
{"response":"queue","queue":[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":["info1","info2"],"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":["A piece of info"],"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}],"command":"QUEUE"}
{"response":"queue","queue":[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":{"INFO2":"Nope","INFO3":"heh"},"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":{"INFO":"yuppers"},"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}],"command":"QUEUE"}
BANNER %
REM 29 MSG One
-(2): Trig = Thursday, 29 February, 2024
@@ -24503,6 +24503,7 @@ TRANSLATE "Cannot open `%s' for writing: %s" ""
TRANSLATE "Cannot stat %s - not running as daemon!" ""
TRANSLATE "Cannot use AT clause in multitrig() function" ""
TRANSLATE "Do not use ["["]] around expression in SET command" ""
TRANSLATE "Duplicate INFO headers are not permitted" ""
TRANSLATE "Error: THROUGH date earlier than start date" ""
TRANSLATE "Executing `%s' for INCLUDECMD and caching as `%s'" ""
TRANSLATE "Found cached directory listing for `%s'" ""
@@ -24510,6 +24511,7 @@ TRANSLATE "Function `%s' defined at %s:%d should take %d argument%s, but actuall
TRANSLATE "Function `%s' redefined (previously defined at %s:%d)" ""
TRANSLATE "GetValidHebDate: Bad adarbehave value %d" ""
TRANSLATE "In" ""
TRANSLATE "Invalid INFO string: Must be of the form \"Header: Value\"" ""
TRANSLATE "Invalid translation: Both original and translated must have the same printf-style formatting sequences in the same order." ""
TRANSLATE "Missing REM type; assuming MSG" ""
TRANSLATE "No Adar A in %d" ""
@@ -24655,11 +24657,16 @@ February 2024 29 4 0
Sunday Monday Tuesday Wednesday Thursday Friday Saturday
January 31
March 31
{"date":"2024-02-07","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-14","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-21","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-28","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-07","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-14","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-21","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
{"date":"2024-02-28","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"}
# rem2ps2 end
-stdin-(1): Invalid INFO string: Must be of the form "Header: Value"
-stdin-(2): Invalid INFO string: Must be of the form "Header: Value"
-stdin-(3): Invalid INFO string: Must be of the form "Header: Value"
-stdin-(6): Duplicate INFO headers are not permitted
No reminders.
Agenda pel dijous, 1 de febrer de 2024:
Language: ca