Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions cmd/things/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -927,3 +927,67 @@ func TestRunJSONRendersTypeAsString(t *testing.T) {
})
}
}

// numericStartField matches a `start` field whose value is a bare number, in
// either the indented or the compact encoding. `startBucket` and `startDate`
// are deliberately not matched: the colon has to follow `start` directly.
var numericStartField = regexp.MustCompile(`"start":\s*-?\d`)

// `start` renders as a string on every JSON surface that carries it —
// `inbox`, `anytime` or `someday`, never the raw Things code (issue #241).
// The assertions are against the raw JSON rather than an unmarshalled
// model.Task on purpose: Start.UnmarshalJSON still accepts the legacy integer,
// so decoding would keep passing even if the encoder regressed to emitting
// ints. This mirrors TestRunJSONRendersTypeAsString, which guards `type` the
// same way.
func TestRunJSONRendersStartAsString(t *testing.T) {
sqlDB := dbtest.NewSQL(t)
today := int64(model.ThingsDateFromTime(time.Now()))
if _, err := sqlDB.Exec(
`INSERT INTO TMTask
(uuid, title, type, status, trashed, start, startBucket, startDate, "index") VALUES
('todo-inbox', 'Sort post', 0, 0, 0, 0, 0, NULL, 1),
('todo-today', 'Buy milk', 0, 0, 0, 1, 0, ?, 2),
('todo-someday', 'Learn Welsh', 0, 0, 0, 2, 0, NULL, 3),
('proj-audit', 'Runbook audit', 1, 0, 0, 1, 0, ?, 4)`,
today, today,
); err != nil {
t.Fatalf("seed: %v", err)
}
database := db.NewFromSQL(sqlDB)

cases := []struct {
name string
args []string
want []string
}{
{"inbox", []string{"--json", "list", "inbox"}, []string{`"start": "inbox"`}},
{"today", []string{"--json", "list", "today"}, []string{`"start": "anytime"`}},
{"someday", []string{"--json", "list", "someday"}, []string{`"start": "someday"`}},
{"show", []string{"--json", "show", "todo-someday"}, []string{`"start": "someday"`}},
{"search", []string{"--json", "search", "Welsh"}, []string{`"start": "someday"`}},
// Projects carry start under the same name and the same codec (issue
// #202), so the field has to change on that surface too.
{"projects", []string{"--json", "projects"}, []string{`"start": "anytime"`}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := runOut(t, database, tc.args...)
if err != nil {
t.Fatalf("run %v: %v", tc.args, err)
}
for _, want := range tc.want {
if !strings.Contains(out, want) {
t.Errorf("missing %s in:\n%s", want, out)
}
}
// Match any numeric token, not just 0/1/2: MarshalJSON falls back
// to the raw integer for a code it does not recognize, so pinning
// the three known codes would miss exactly the case that can
// legitimately emit one.
if loc := numericStartField.FindString(out); loc != "" {
t.Errorf("raw Things start code leaked into JSON (%q):\n%s", loc, out)
}
})
}
}
9 changes: 9 additions & 0 deletions docs/content/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ Every command accepts `-j` / `--json`, and it changes more than the format:
become `.type=="project"`. It is not the vocabulary an `import` payload
takes: that format is Things' own and spells a to-do `"to-do"`, so do
not copy `.type` from a listing into an import item.
- **Start is a string enum too**, `"inbox"`, `"anytime"` or `"someday"`,
not the raw Things integer. It is the list an item falls back to when it
carries no date, so it does not on its own say which list the app shows
the item in: a dated `"anytime"` row is in Today, a dated `"someday"` row
is in Upcoming, and only an undated one is in Someday. In v0.7.0 and
earlier this field was the integer `0`, `1` or `2`, so a filter matching
on `.start==2` has to become `.start=="someday"`. `startBucket` beside it
is still an integer — `1` is the app's This Evening section, `0` is
everything else.
- **Projects carry scheduling too.** `things projects` reports `start`,
`startBucket`, `startDate` and `deadline` under the same names and
encodings a to-do uses, so a scheduled project reads the same way
Expand Down
27 changes: 19 additions & 8 deletions docs/content/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@ Read commands (`list`/views, `projects`, `areas`, `tags`, `show`, `search`)
accept `-j` / `--json` for structured output. Run `things --help` or
`things <subcommand> --help` for the full flag list.

In JSON, `status` and `type` are string enums rather than the raw Things
integers. `status` is `"open"`, `"completed"` or `"cancelled"`, and appears
on to-dos, projects and checklist items. `type` is `"task"` or `"project"`,
and appears on task rows only — `projects`, `areas` and `tags` rows carry no
`type`. Headings are never returned by any command, so the third Things type
never reaches the output. In v0.7.0 and earlier `type` was the integer `0`,
`1` or `2`, so a caller matching on `.type==1` has to become
`.type=="project"`.
In JSON, `status`, `type` and `start` are string enums rather than the raw
Things integers. `status` is `"open"`, `"completed"` or `"cancelled"`, and
appears on to-dos, projects and checklist items. `type` is `"task"` or
`"project"`, and appears on task rows only — `projects`, `areas` and `tags`
rows carry no `type`. Headings are never returned by any command, so the
third Things type never reaches the output.

`start` is `"inbox"`, `"anytime"` or `"someday"`, and appears on to-do and
project rows. It is the list an item falls back to when it carries no date,
so it does not on its own say which list the app shows the item in: a dated
`"anytime"` row is in Today, a dated `"someday"` row is in Upcoming, and
only an undated one is in Someday.

In v0.7.0 and earlier `type` and `start` were both integers, so a caller
matching on `.type==1` has to become `.type=="project"`, and one matching
on `.start==2` has to become `.start=="someday"`. `startBucket` alongside
them is still an integer: `1` is the app's This Evening section and `0` is
everything else. Only the first of those has a name in Things' own
vocabulary, so naming the pair would have meant inventing a word for `0`.

The `type` values a listing reports are not the ones an `import` payload
takes: that payload is Things' own JSON URL scheme, which spells a to-do
Expand Down
Loading
Loading