Skip to content

Commit 14f7623

Browse files
authored
Merge pull request #19 from doneill/task/jdo-38-get-subjects-cmd
Fetch subjects updated since
2 parents 730748b + f68a288 commit 14f7623

File tree

4 files changed

+343
-0
lines changed

4 files changed

+343
-0
lines changed

api/apisubjects.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
)
7+
8+
// ----------------------------------------------
9+
// structs
10+
// ----------------------------------------------
11+
12+
type SubjectsResponse struct {
13+
Data []Subject `json:"data"`
14+
Status struct {
15+
Code int `json:"code"`
16+
Message string `json:"message"`
17+
} `json:"status"`
18+
}
19+
20+
type Subject struct {
21+
ID string `json:"id"`
22+
LastPosition LastPosition `json:"last_position"`
23+
LastPositionDate string `json:"last_position_date"`
24+
Name string `json:"name"`
25+
SubjectSubtype string `json:"subject_subtype"`
26+
SubjectType string `json:"subject_type"`
27+
}
28+
29+
type LastPosition struct {
30+
Geometry Geometry `json:"geometry"`
31+
Properties Properties `json:"properties"`
32+
Type string `json:"type"`
33+
}
34+
35+
type Geometry struct {
36+
Coordinates []float64 `json:"coordinates"`
37+
Type string `json:"type"`
38+
}
39+
40+
type Properties struct {
41+
DateTime string `json:"DateTime"`
42+
CoordinateProperties Coordinate `json:"coordinateProperties"`
43+
ID string `json:"id"`
44+
Image string `json:"image"`
45+
LastVoiceCallStartAt *string `json:"last_voice_call_start_at"`
46+
LocationRequestedAt *string `json:"location_requested_at"`
47+
RadioState string `json:"radio_state"`
48+
RadioStateAt string `json:"radio_state_at"`
49+
Stroke string `json:"stroke"`
50+
StrokeOpacity float64 `json:"stroke-opacity"`
51+
StrokeWidth int `json:"stroke-width"`
52+
SubjectSubtype string `json:"subject_subtype"`
53+
SubjectType string `json:"subject_type"`
54+
Title string `json:"title"`
55+
}
56+
57+
type Coordinate struct {
58+
Time string `json:"time"`
59+
}
60+
61+
// ----------------------------------------------
62+
// Client methods
63+
// ----------------------------------------------
64+
65+
func (c *Client) Subjects(updatedSince string) (*SubjectsResponse, error) {
66+
params := url.Values{}
67+
params.Add("updated_since", updatedSince)
68+
69+
endpoint := fmt.Sprintf("%s?%s", API_SUBJECTS, params.Encode())
70+
71+
req, err := c.newRequest("GET", endpoint, false)
72+
if err != nil {
73+
return nil, fmt.Errorf("error generating subjects request: %w", err)
74+
}
75+
76+
var responseData SubjectsResponse
77+
if err := c.doRequest(req, &responseData); err != nil {
78+
return nil, fmt.Errorf("error fetching subjects: %w", err)
79+
}
80+
81+
return &responseData, nil
82+
}

api/ersvc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const API_V1 = "/api/v1.0"
1616

1717
const API_AUTH = "/oauth2/token"
1818

19+
const API_SUBJECTS = API_V1 + "/subjects"
20+
1921
const API_USER = API_V1 + "/user"
2022

2123
const API_USER_ME = API_USER + "/me"

api/subjects_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"strings"
8+
"testing"
9+
"time"
10+
)
11+
12+
func TestSubjects(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
updatedSince string
16+
serverResponse *SubjectsResponse
17+
statusCode int
18+
expectError bool
19+
}{
20+
{
21+
name: "successful response",
22+
updatedSince: time.Now().AddDate(0, 0, -3).UTC().Format("2006-01-02T15:04:05.000"),
23+
serverResponse: &SubjectsResponse{
24+
Data: []Subject{
25+
{
26+
ID: "123778d5-ffcc-4911-8d3b-e43cfdb426f7",
27+
Name: "Test Subject",
28+
LastPosition: LastPosition{
29+
Geometry: Geometry{
30+
Coordinates: []float64{-121.6670876888658, 47.44309785582009},
31+
Type: "Point",
32+
},
33+
Properties: Properties{
34+
DateTime: "2024-12-23T18:34:51+00:00",
35+
Title: "Test Subject",
36+
},
37+
Type: "Feature",
38+
},
39+
LastPositionDate: "2024-12-23T18:34:51+00:00",
40+
SubjectType: "person",
41+
SubjectSubtype: "ranger",
42+
},
43+
},
44+
Status: struct {
45+
Code int `json:"code"`
46+
Message string `json:"message"`
47+
}{
48+
Code: 200,
49+
Message: "OK",
50+
},
51+
},
52+
statusCode: http.StatusOK,
53+
expectError: false,
54+
},
55+
{
56+
name: "server error",
57+
updatedSince: time.Now().AddDate(0, 0, -3).UTC().Format("2006-01-02T15:04:05.000"),
58+
serverResponse: nil,
59+
statusCode: http.StatusInternalServerError,
60+
expectError: true,
61+
},
62+
{
63+
name: "empty response",
64+
updatedSince: time.Now().AddDate(0, 0, -3).UTC().Format("2006-01-02T15:04:05.000"),
65+
serverResponse: &SubjectsResponse{
66+
Data: []Subject{},
67+
Status: struct {
68+
Code int `json:"code"`
69+
Message string `json:"message"`
70+
}{
71+
Code: 200,
72+
Message: "OK",
73+
},
74+
},
75+
statusCode: http.StatusOK,
76+
expectError: false,
77+
},
78+
}
79+
80+
for _, tt := range tests {
81+
t.Run(tt.name, func(t *testing.T) {
82+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83+
if r.Method != http.MethodGet {
84+
t.Errorf("expected GET request, got %s", r.Method)
85+
}
86+
if r.Header.Get("Authorization") != "Bearer testtoken" {
87+
t.Errorf("expected Bearer testtoken, got %s", r.Header.Get("Authorization"))
88+
}
89+
90+
query := r.URL.Query()
91+
updatedSince := query.Get("updated_since")
92+
if updatedSince == "" {
93+
t.Error("expected updated_since parameter, got none")
94+
}
95+
96+
if !strings.HasSuffix(r.URL.Path, API_SUBJECTS) {
97+
t.Errorf("expected path to end with %s, got %s", API_SUBJECTS, r.URL.Path)
98+
}
99+
100+
w.WriteHeader(tt.statusCode)
101+
if tt.serverResponse != nil {
102+
if err := json.NewEncoder(w).Encode(tt.serverResponse); err != nil {
103+
t.Errorf("failed to encode response: %v", err)
104+
return
105+
}
106+
}
107+
}))
108+
defer server.Close()
109+
110+
client := ERClient("test", "testtoken", server.URL)
111+
resp, err := client.Subjects(tt.updatedSince)
112+
113+
if tt.expectError && err == nil {
114+
t.Error("expected error, got nil")
115+
}
116+
if !tt.expectError && err != nil {
117+
t.Errorf("unexpected error: %v", err)
118+
}
119+
120+
if !tt.expectError && tt.serverResponse != nil {
121+
if resp == nil {
122+
t.Fatal("expected response, got nil")
123+
}
124+
125+
if len(resp.Data) != len(tt.serverResponse.Data) {
126+
t.Errorf("expected %d subjects, got %d",
127+
len(tt.serverResponse.Data), len(resp.Data))
128+
}
129+
130+
if len(resp.Data) > 0 {
131+
expectedSubject := tt.serverResponse.Data[0]
132+
actualSubject := resp.Data[0]
133+
134+
if actualSubject.ID != expectedSubject.ID {
135+
t.Errorf("expected subject ID %s, got %s",
136+
expectedSubject.ID, actualSubject.ID)
137+
}
138+
139+
if actualSubject.Name != expectedSubject.Name {
140+
t.Errorf("expected subject name %s, got %s",
141+
expectedSubject.Name, actualSubject.Name)
142+
}
143+
144+
if len(actualSubject.LastPosition.Geometry.Coordinates) != 2 {
145+
t.Error("expected coordinates to have latitude and longitude")
146+
}
147+
}
148+
149+
if resp.Status.Code != tt.serverResponse.Status.Code {
150+
t.Errorf("expected status code %d, got %d",
151+
tt.serverResponse.Status.Code, resp.Status.Code)
152+
}
153+
}
154+
})
155+
}
156+
}

cmd/subjects.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"time"
8+
9+
"github.com/doneill/er-cli/api"
10+
"github.com/doneill/er-cli/config"
11+
"github.com/olekukonko/tablewriter"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
var daysAgo int
16+
17+
// ----------------------------------------------
18+
// subjects command
19+
// ----------------------------------------------
20+
21+
var subjectsCmd = &cobra.Command{
22+
Use: "subjects",
23+
Short: "Get updated subjects data",
24+
Long: `Return subject data updated within specified number of days ago`,
25+
Run: func(cmd *cobra.Command, args []string) {
26+
subjects()
27+
},
28+
}
29+
30+
// ----------------------------------------------
31+
// functions
32+
// ----------------------------------------------
33+
34+
func subjects() {
35+
updatedSince := time.Now().AddDate(0, 0, -daysAgo).UTC().Format("2006-01-02T15:04:05.000")
36+
client := api.ERClient(config.Sitename(), config.Token())
37+
38+
subjectsResponse, err := client.Subjects(updatedSince)
39+
if err != nil {
40+
log.Fatalf("Error getting subjects: %v", err)
41+
}
42+
43+
if subjectsResponse == nil || len(subjectsResponse.Data) == 0 {
44+
fmt.Println("No subjects found")
45+
return
46+
}
47+
48+
table := configureSubjectsTable()
49+
for _, subject := range subjectsResponse.Data {
50+
table.Append(formatSubjectData(&subject))
51+
}
52+
53+
table.Render()
54+
}
55+
56+
func formatSubjectData(subject *api.Subject) []string {
57+
coordinates := "N/A"
58+
59+
if len(subject.LastPosition.Geometry.Coordinates) >= 2 {
60+
coordinates = fmt.Sprintf("%.6f, %.6f",
61+
subject.LastPosition.Geometry.Coordinates[1],
62+
subject.LastPosition.Geometry.Coordinates[0],
63+
)
64+
}
65+
66+
return []string{
67+
subject.Name,
68+
subject.ID,
69+
subject.SubjectType,
70+
subject.SubjectSubtype,
71+
subject.LastPositionDate,
72+
coordinates,
73+
}
74+
}
75+
76+
func configureSubjectsTable() *tablewriter.Table {
77+
table := tablewriter.NewWriter(os.Stdout)
78+
table.SetHeader([]string{
79+
"Name",
80+
"ID",
81+
"Type",
82+
"Subtype",
83+
"Last Position Date",
84+
"Last Position (lat, lon)",
85+
})
86+
table.SetBorders(tablewriter.Border{
87+
Left: true,
88+
Top: true,
89+
Right: true,
90+
Bottom: true,
91+
})
92+
table.SetCenterSeparator("|")
93+
return table
94+
}
95+
96+
// ----------------------------------------------
97+
// initialize
98+
// ----------------------------------------------
99+
100+
func init() {
101+
rootCmd.AddCommand(subjectsCmd)
102+
subjectsCmd.Flags().IntVarP(&daysAgo, "updated-since", "u", 3, "Number of days ago to query updates from")
103+
}

0 commit comments

Comments
 (0)