/
annotations_rw.go
155 lines (129 loc) · 4.83 KB
/
annotations_rw.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package annotations
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
tidUtils "github.com/Financial-Times/transactionid-utils-go"
log "github.com/sirupsen/logrus"
)
const rwURLPattern = "%s/drafts/content/%s/annotations"
const DocumentHashHeader = "Document-Hash"
const PreviousDocumentHashHeader = "Previous-Document-Hash"
type RW interface {
Read(ctx context.Context, contentUUID string) (*Annotations, string, bool, error)
Write(ctx context.Context, contentUUID string, annotations *Annotations, hash string) (string, error)
Endpoint() string
GTG() error
}
type annotationsRW struct {
endpoint string
httpClient *http.Client
}
func NewRW(client *http.Client, endpoint string) RW {
return &annotationsRW{endpoint, client}
}
var ErrUnexpectedStatusRead = errors.New("annotations RW returned an unexpected HTTP status code in read operation")
var ErrUnexpectedStatusWrite = errors.New("annotations RW returned an unexpected HTTP status code in write operation")
var ErrGTGNotOK = errors.New("gtg returned a non-200 HTTP status")
func (rw *annotationsRW) Read(ctx context.Context, contentUUID string) (*Annotations, string, bool, error) {
tid, err := tidUtils.GetTransactionIDFromContext(ctx)
if err != nil {
tid = tidUtils.NewTransactionID()
log.WithField(tidUtils.TransactionIDKey, tid).
WithField("uuid", contentUUID).
WithError(err).
Warn("Transaction ID error in getting annotations from RW with concept data: Generated a new transaction ID")
ctx = tidUtils.TransactionAwareContext(ctx, tid)
}
readLog := log.WithField(tidUtils.TransactionIDKey, tid).WithField("uuid", contentUUID)
req, err := http.NewRequest("GET", fmt.Sprintf(rwURLPattern, rw.endpoint, contentUUID), nil)
if err != nil {
readLog.WithError(err).Error("Error in creating the HTTP read request to annotations RW")
return nil, "", false, err
}
resp, err := rw.httpClient.Do(req.WithContext(ctx))
if err != nil {
readLog.WithError(err).Error("Error making the HTTP read request to annotations RW")
return nil, "", false, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
var annotations Annotations
err = json.NewDecoder(resp.Body).Decode(&annotations)
if err != nil {
readLog.WithError(err).Error("Error in unmarshalling the HTTP response from annotations RW")
return nil, "", false, err
}
hash := resp.Header.Get(DocumentHashHeader)
return &annotations, hash, true, nil
case http.StatusNotFound:
return nil, "", false, nil
default:
return nil, "", false, fmt.Errorf("status %d: %w", resp.StatusCode, ErrUnexpectedStatusRead)
}
}
func (rw *annotationsRW) Write(ctx context.Context, contentUUID string, annotations *Annotations, hash string) (string, error) {
tid, err := tidUtils.GetTransactionIDFromContext(ctx)
if err != nil {
tid = tidUtils.NewTransactionID()
log.WithField(tidUtils.TransactionIDKey, tid).
WithField("uuid", contentUUID).
WithError(err).
Warn("Transaction ID error in writing annotations to RW with concept data: Generated a new transaction ID")
ctx = tidUtils.TransactionAwareContext(ctx, tid)
}
writeLog := log.WithField(tidUtils.TransactionIDKey, tid).WithField("uuid", contentUUID)
annotationsBody, err := json.Marshal(annotations)
if err != nil {
writeLog.WithError(err).Error("Unable to marshall annotations that needs to be written")
return "", err
}
req, err := http.NewRequest("PUT", fmt.Sprintf(rwURLPattern, rw.endpoint, contentUUID), bytes.NewBuffer(annotationsBody))
if err != nil {
writeLog.WithError(err).Error("Error in creating the HTTP write request to annotations RW")
return "", err
}
req.Header.Set(PreviousDocumentHashHeader, hash)
resp, err := rw.httpClient.Do(req.WithContext(ctx))
if err != nil {
writeLog.WithError(err).Error("Error making the HTTP request to annotations RW")
return "", err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK, http.StatusCreated:
newHash := resp.Header.Get(DocumentHashHeader)
return newHash, nil
default:
return "", fmt.Errorf("status %d: %w", resp.StatusCode, ErrUnexpectedStatusWrite)
}
}
func (rw *annotationsRW) Endpoint() string {
return rw.endpoint
}
func (rw *annotationsRW) GTG() error {
req, err := http.NewRequest("GET", rw.endpoint+"/__gtg", nil)
if err != nil {
log.WithError(err).Error("Error in creating the HTTP request to annotations RW GTG")
return fmt.Errorf("GTG: %w", err)
}
resp, err := rw.httpClient.Do(req)
if err != nil {
log.WithError(err).Error("Error making the HTTP request to annotations RW GTG")
return fmt.Errorf("GTG: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("status %d: %w", resp.StatusCode, ErrGTGNotOK)
}
return fmt.Errorf("status %d %s: %w", resp.StatusCode, string(body), ErrGTGNotOK)
}
return nil
}