Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 733b4b1

Browse files
authored
Small improvements to Stock Ticker service (#21)
This PR exposes a REST endpoint for the Stock Ticker service so REST clients could query the service for the latest price for a symbol. Unlike with the WebSocket version only one response will be generated for the request. Also as a minor improvement to the experience, now a request made either through REST or WebSocket will be responded immediately instead of after 30 seconds have passed. Signed-off-by: Josh Kim <[email protected]>
1 parent 47a1f3e commit 733b4b1

File tree

2 files changed

+126
-62
lines changed

2 files changed

+126
-62
lines changed

plank/README.md

+16-6
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ BUILD_OS=darwin|linux|windows go run build.go
4444

4545
Once successfully built, `plank` binary will be ready under `build/`.
4646

47-
> NOTE: we acknowledge there's a lack of build script for Windows Powershell, We'll add it soon!
48-
4947
### Generate a self signed certificate
5048
Plank can run in non-HTTPS mode but it's generally a good idea to always do development in a similar environment where you'll be serving your
5149
audience in public internet (or even intranet). Plank repository comes with a handy utility script that can generate a pair of server certificate
@@ -80,12 +78,24 @@ SPA static assets /assets
8078
Health endpoint /health
8179
Prometheus endpoint /prometheus
8280
...
83-
time="2021-08-05T21:32:50-07:00" level=info msg="Starting HTTP server at localhost:30080 with TLS" fileName=server.go goroutine=28 package=server
81+
time="2021-08-17T13:28:15-07:00" level=info msg="Service '*services.StockTickerService' initialized successfully" fileName=initialize.go goroutine=44 package=server
82+
time="2021-08-17T13:28:15-07:00" level=info msg="Service channel 'stock-ticker-service' is now bridged to a REST endpoint /rest/stock-ticker/{symbol} (GET)\n" fileName=server.go goroutine=44 package=server
83+
time="2021-08-17T13:28:15-07:00" level=info msg="Starting Fabric broker at localhost:30080/ws" fileName=server.go goroutine=1 package=server
84+
time="2021-08-17T13:28:15-07:00" level=info msg="Starting HTTP server at localhost:30080 with TLS" fileName=server.go goroutine=3 package=server
8485
```
8586
86-
Open your browser and navigate to https://localhost:30080, accept the self-signed certificate warning and you'll be greeted with a 404!
87-
This is an expected behavior, as the demo app does not serve anything at root `/`, but we will consider changing the default 404 screen to
88-
something that looks more informational or more appealing at least.
87+
Now, open your browser and navigate to https://localhost:30080/rest/stock-ticker/VMW (or
88+
type `curl -k https://localhost:30080/rest/stock-ticker/VMW` in Terminal if you prefer CLI),
89+
and accept the self-signed certificate warning. You will be served a page that shows the latest stock price
90+
for VMware, Inc. Try and swap out `VMW` with another symbol of your choice to further test it out!
91+
92+
> NOTE: The sample service is using a loosely gated third party API which imposes
93+
> a substantial limit on how many calls you can make per minute and per day in return for making
94+
> the service free to all.
95+
96+
> NOTE: If you navigate to the root at https://localhost:30080, you'll be greeted with a 404!
97+
> This is an expected behavior, as the demo app does not serve anything at root `/`, but we will
98+
> consider changing the default 404 screen to something that is informational or more appealing at least.
8999
90100
## All supported flags and usages
91101

plank/services/stock-ticker-service.go

+110-56
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ package services
66
import (
77
"context"
88
"encoding/json"
9+
"fmt"
10+
"github.com/google/uuid"
11+
"github.com/gorilla/mux"
912
"github.com/vmware/transport-go/bus"
1013
"github.com/vmware/transport-go/model"
1114
"github.com/vmware/transport-go/plank/utils"
@@ -22,22 +25,23 @@ import (
2225

2326
const (
2427
StockTickerServiceChannel = "stock-ticker-service"
25-
StockTickerAPI = "https://www.alphavantage.co/query"
28+
StockTickerAPI = "https://www.alphavantage.co/query"
2629
)
2730

2831
// TickerSnapshotData and TickerMetadata ares the data structures for this demo service
2932
type TickerSnapshotData struct {
30-
MetaData *TickerMetadata `json:"Meta Data"`
33+
MetaData *TickerMetadata `json:"Meta Data"`
3134
TimeSeries map[string]map[string]interface{} `json:"Time Series (1min)"`
35+
Note string `json:"Note"`
3236
}
3337

3438
type TickerMetadata struct {
35-
Information string `json:"1. Information"`
36-
Symbol string `json:"2. Symbol"`
39+
Information string `json:"1. Information"`
40+
Symbol string `json:"2. Symbol"`
3741
LastRefreshed string `json:"3. Last Refreshed"`
38-
Interval string `json:"4. Interval"`
39-
OutputSize string `json:"5. Output Size"`
40-
TimeZone string `json:"6. Time Zone"`
42+
Interval string `json:"4. Interval"`
43+
OutputSize string `json:"5. Output Size"`
44+
TimeZone string `json:"6. Time Zone"`
4145
}
4246

4347
// StockTickerService is a more complex real life example where its job is to subscribe clients
@@ -47,9 +51,9 @@ type TickerMetadata struct {
4751
// once the service receives the request, it will schedule a job to query the stock price API
4852
// for the provided symbol, retrieve the data and pipe it back to the client every thirty seconds.
4953
// upon the connected client leaving, the service will remove from its cache the timer.
50-
type StockTickerService struct{
54+
type StockTickerService struct {
5155
tickerListenersMap map[string]*time.Ticker
52-
lock sync.RWMutex
56+
lock sync.RWMutex
5357
}
5458

5559
// NewStockTickerService returns a new instance of StockTickerService
@@ -63,11 +67,31 @@ func NewStockTickerService() *StockTickerService {
6367
// a third party API and return the results back to the user.
6468
func (ps *StockTickerService) HandleServiceRequest(request *model.Request, core service.FabricServiceCore) {
6569
switch request.Request {
66-
case "receive_ticker_updates":
70+
case "ticker_price_lookup":
71+
input := request.Payload.(map[string]string)
72+
response, err := queryStockTickerAPI(input["symbol"])
73+
if err != nil {
74+
core.SendErrorResponse(request, 400, err.Error())
75+
return
76+
}
77+
// send the response back to the client
78+
core.SendResponse(request, response)
79+
break
80+
81+
case "ticker_price_update_stream":
6782
// parse the request and extract user input from key "symbol"
6883
input := request.Payload.(map[string]interface{})
6984
symbol := input["symbol"].(string)
7085

86+
// get the price immediately for the first request
87+
response, err := queryStockTickerAPI(symbol)
88+
if err != nil {
89+
core.SendErrorResponse(request, 400, err.Error())
90+
return
91+
}
92+
// send the response back to the client
93+
core.SendResponse(request, response)
94+
7195
// set a ticker that fires every 30 seconds and keep it in a map for later disposal
7296
ps.lock.Lock()
7397
ticker := time.NewTicker(30 * time.Second)
@@ -79,57 +103,18 @@ func (ps *StockTickerService) HandleServiceRequest(request *model.Request, core
79103
for {
80104
select {
81105
case <-ticker.C:
82-
// craft a new HTTP request for the stock price provider API
83-
req, err := newTickerRequest(symbol)
84-
if err != nil {
85-
core.SendErrorResponse(request, 400, err.Error())
86-
continue
87-
}
88-
89-
// perform an HTTP call
90-
rsp, err := ctxhttp.Do(context.Background(), http.DefaultClient, req)
91-
if err != nil {
92-
core.SendErrorResponse(request, rsp.StatusCode, err.Error())
93-
continue
94-
}
95-
96-
// parse the response from the HTTP call
97-
defer rsp.Body.Close()
98-
tickerData := &TickerSnapshotData{}
99-
b, err := ioutil.ReadAll(rsp.Body)
100-
if err != nil {
101-
core.SendErrorResponse(request, 500, err.Error())
102-
continue
103-
}
104-
105-
if err = json.Unmarshal(b, tickerData); err != nil {
106-
core.SendErrorResponse(request, 500, err.Error())
107-
continue
108-
}
109-
110-
if tickerData == nil || tickerData.TimeSeries == nil {
111-
core.SendErrorResponse(request, 500, string(b))
112-
continue
113-
}
114-
115-
// extract the data we need.
116-
latestClosePriceStr := tickerData.TimeSeries[tickerData.MetaData.LastRefreshed]["4. close"].(string)
117-
latestClosePrice, err := strconv.ParseFloat(latestClosePriceStr, 32)
106+
response, err = queryStockTickerAPI(symbol)
118107
if err != nil {
119108
core.SendErrorResponse(request, 500, err.Error())
120109
continue
121110
}
122111

123112
// log message to demonstrate that once the client disconnects
124113
// the server disposes of the ticker to prevent memory leak.
125-
utils.Log.Warnln("sending...")
114+
utils.Log.Infoln("sending...")
126115

127116
// send the response back to the client
128-
core.SendResponse(request, map[string]interface{}{
129-
"symbol": symbol,
130-
"lastRefreshed": tickerData.MetaData.LastRefreshed,
131-
"closePrice": latestClosePrice,
132-
})
117+
core.SendResponse(request, response)
133118
}
134119
}
135120
}()
@@ -173,10 +158,27 @@ func (ps *StockTickerService) OnServerShutdown() {
173158
return
174159
}
175160

176-
// GetRESTBridgeConfig returns nothing. this service is only available through
177-
// STOMP over WebSocket.
161+
// GetRESTBridgeConfig returns a config for a REST endpoint that performs the same action as the STOMP variant
162+
// except that there will be only one response instead of every 30 seconds.
178163
func (ps *StockTickerService) GetRESTBridgeConfig() []*service.RESTBridgeConfig {
179-
return nil
164+
return []*service.RESTBridgeConfig{
165+
{
166+
ServiceChannel: StockTickerServiceChannel,
167+
Uri: "/rest/stock-ticker/{symbol}",
168+
Method: http.MethodGet,
169+
AllowHead: true,
170+
AllowOptions: true,
171+
FabricRequestBuilder: func(w http.ResponseWriter, r *http.Request) model.Request {
172+
pathParams := mux.Vars(r)
173+
return model.Request{
174+
Id: &uuid.UUID{},
175+
Payload: map[string]string{"symbol": pathParams["symbol"]},
176+
Request: "ticker_price_lookup",
177+
BrokerDestination: nil,
178+
}
179+
},
180+
},
181+
}
180182
}
181183

182184
// newTickerRequest is a convenient function that takes symbol as an input and returns
@@ -194,4 +196,56 @@ func newTickerRequest(symbol string) (*http.Request, error) {
194196
}
195197
req.URL.RawQuery = uv.Encode()
196198
return req, nil
197-
}
199+
}
200+
201+
// queryStockTickerAPI performs an HTTP request against the Stock Ticker API and returns the results
202+
// as a generic map[string]interface{} structure. if there's any error during the request-response cycle
203+
// a nil will be returned followed by an error object.
204+
func queryStockTickerAPI(symbol string) (map[string]interface{}, error) {
205+
// craft a new HTTP request for the stock price provider API
206+
req, err := newTickerRequest(symbol)
207+
if err != nil {
208+
return nil, err
209+
}
210+
211+
// perform an HTTP call
212+
rsp, err := ctxhttp.Do(context.Background(), http.DefaultClient, req)
213+
if err != nil {
214+
return nil, err
215+
}
216+
217+
// parse the response from the HTTP call
218+
defer rsp.Body.Close()
219+
tickerData := &TickerSnapshotData{}
220+
b, err := ioutil.ReadAll(rsp.Body)
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
if err = json.Unmarshal(b, tickerData); err != nil {
226+
return nil, err
227+
}
228+
229+
// Alpha Vantage which is the provider of this API limits API calls to 5 calls per minute and 500 a day, and when
230+
// the quota has been reached it will return a message in the Note field.
231+
if len(tickerData.Note) > 0 {
232+
return nil, fmt.Errorf(tickerData.Note)
233+
}
234+
235+
if tickerData == nil || tickerData.TimeSeries == nil {
236+
return nil, err
237+
}
238+
239+
// extract the data we need.
240+
latestClosePriceStr := tickerData.TimeSeries[tickerData.MetaData.LastRefreshed]["4. close"].(string)
241+
latestClosePrice, err := strconv.ParseFloat(latestClosePriceStr, 32)
242+
if err != nil {
243+
return nil, err
244+
}
245+
246+
return map[string]interface{}{
247+
"symbol": symbol,
248+
"lastRefreshed": tickerData.MetaData.LastRefreshed,
249+
"closePrice": latestClosePrice,
250+
}, nil
251+
}

0 commit comments

Comments
 (0)