diff --git a/README.md b/README.md index d22e003..8b3323e 100755 --- a/README.md +++ b/README.md @@ -9,222 +9,179 @@ Bridges is a Chainlink adaptor framework, lowering the barrier of entry for anyone to create their own: -- Bridges CLI application, allowing you to quickly run an adaptor. -- Create adaptors with an easy to interpret JSON schema. -- Simple interface to implement your own custom adaptors that can do anything. -- Supports running in serverless environments such as AWS Lambda & GCP functions. +- A tested hardened library that removes the need to build your own HTTP server, allowing you to just focus on +adapter requirements. +- Simple interface to allow you to build an adapter that confides to Chainlink schema. +- Kept up to date with any changes, meaning no extra work for existing adapters to support new schema changes or +features. +- Supports running in serverless environments such as AWS Lambda & GCP functions with minimal effort. ## Contents -1. [Install](#install) -2. [Usage](#usage) -3. [Managing API Keys](#managing-api-keys) -4. [Testing your Bridge](#testing-your-bridge) -5. [Chainlink Integration](#chainlink-integration) -6. [Bridge JSON](#bridge-json) -7. [Examples](#examples) +1. [Code Examples](#code-examples) +2. [Running in AWS Lambda](#running-in-aws-lambda) +3. [Running in GCP Functions](#running-in-gcp-functions) +4. [Example Implementations](#example-implementations) + - [Basic](#basic) + - [Unauthenticated HTTP Calls](#unauthenticated-http-calls) + - [Authenticated HTTP Calls](#authenticated-http-calls) -## Install +## Code Examples -View the [releases page](https://github.com/linkpoolio/bridges/releases) and download the latest version for your -operating system, then add it to PATH. - -## Usage - -### Quick Usage - -For the simplest adaptor, run the following: -``` -bridges -b https://s3.linkpool.io/bridges/cryptocompare.json -``` -Once running, the adaptor will be started on port 8080. - -### Usage -``` -Usage of bridges: - -b, --bridge string Filepath/URL of bridge JSON file - -p, --port int Server port (default 8080) +- [CryptoCompare](examples/cryptocompare): Simplest example. +- [API Aggregator](examples/apiaggregator): Aggregates multiple endpoints using mean/median/mode. +- [Wolfram Alpha](examples/wolframalpha): Short answers API, non-JSON, uses string splitting. +- [Gas Station](examples/gasstation): Single answer response, no authentication. +- [Asset Price](https://github.com/linkpoolio/asset-price-cl-ea): A more complex example that aggregates crypto asset +prices from multiple exchanges by weighted volume. + +## Running in Docker +After implementing your bridge, if you'd like to run it in Docker, you can reference the Dockerfiles in +[examples](examples/cryptocompare/Dockerfile) to then use as a template for your own Dockerfile. + +## Running in AWS Lambda +After you've completed implementing your bridge, you can then test it in AWS Lambda. To do so: + +1. Build the executable: + ```bash + GO111MODULE=on go build -o bridge + ``` +2. Add the file to a ZIP archive: + ```bash + zip bridge.zip ./bridge + ``` +3. Upload the the zip file into AWS and then use `bridge` as the +handler. +4. Set the `LAMBDA` environment variable to `true` in AWS for +the adaptor to be compatible with Lambda. + +## Running in GCP Functions +To support GCP within your bridge, you need to add an extra function into your bridge as the entrypoint: +```go +func Handler(w http.ResponseWriter, r *http.Request) { + bridges.NewServer(&Example{}).Handler(w, r) +} ``` -With the `-b` flag, either URLs or relative file paths can be specified, for example: -``` -bridges -b https://s3.linkpool.io/bridges/rapidapi.json -``` -is equal to -``` -bridges -b json/rapidapi.json +You can then use the gcloud CLI tool to deploy it, for example: +```bash +gcloud functions deploy bridge --runtime go111 --entry-point Handler ``` -## Managing API Keys +## Example Implementations -Bridges supports passing in API keys on the bridge http calls. These api keys are fed in as environment variables -on running bridges. For example: +### Basic +Bridges works by providing a simple interface to confide to. The interface contains two functions, `Run` and `Opts`. +The `Run` function is called on each HTTP request, `Opts` is called on start-up. Below is a very basic implementation +that returns the `value` as passed in by Chainlink, set back as `newValue` in the response: -``` -API_KEY=my-api-key bridges -b https://s3.linkpool.io/bridges/rapidapi.json -``` - -It's recommended to use secret managers for storing API keys. - -#### Considerations -**API key environment variables may not be named `API_KEY`.** Refer to the JSON file for each variable name, - in `auth.env`. - -Custom implementations of the bridges interface may also completely differ and not use environment variables. +```go +package main -### Lambda Usage +import ( + "github.com/linkpoolio/bridges" +) -View the [releases page](https://github.com/linkpoolio/bridges/releases) and download the Linux x86-64 zip. Upload the -zip into Lambda and set the handler as `bridges`. +type MyAdapter struct{} -Then set the following environment variables: +func (ma *MyAdapter) Run(h *bridge.Helper) (interface{}, error) { + return map[string]string{"newValue": h.GetParam("value")}, nil +} - - `LAMBDA=true` - - `BRIDGE=` - -Since bridges queries the bridge URL each call, it's recommend to host your own JSON files in S3 for latency and -your own redundancy. This is not the case when running locally or using docker. - -### Docker Usage +func (ma *MyAdapter) Opts() *bridge.Opts { + return &bridge.Opts{ + Name: "MyAdapter", + Lambda: true, + } +} -Run by either appending arguments or setting environment variables: -``` -docker run -it linkpool/bridges:latest -b https://s3.linkpool.io/bridges/rapidapi.json +func main() { + bridge.NewServer(&MyAdaptor{}).Start(8080) +} ``` -## Testing your Bridge +### Unauthenticated HTTP Calls +The bridges library provides a helper object that intends to make actions like performing HTTP calls simpler, removing +the need to write extensive error handling or the need to have the knowledge of Go's in-built http libraries. -To test a bridge, you need to send a `POST` request to it in the Chainlink `RunResult` type. For example: +For example, this below implementation uses the `HTTPCall` function to make a simple unauthenticated call to ETH Gas +Station: +```go +package main -Start your bridge: -``` -bridges -b https://s3.linkpool.io/bridges/cryptocompare.json -``` +import ( + "github.com/linkpoolio/bridges" +) -Call it: -``` -curl -X POST -d "{\"jobRunId\":\"1234\",\"data\":{\"key\":\"value\"}}" http://localhost:8080 -``` +type GasStation struct{} -Result: -```json -{ - "jobRunId":"1234", - "status":"completed", - "error":null, - "pending":false, - "data":{ - "EUR":140.88, - "JPY":17717.05, - "USD":159.77, - "key":"value" - } +func (gs *GasStation) Run(h *bridges.Helper) (interface{}, error) { + obj := make(map[string]interface{}) + err := h.HTTPCall( + http.MethodGet, + "https://ethgasstation.info/json/ethgasAPI.json", + &obj, + ) + return obj, err } -``` -## Chainlink Integration - -Once you have a running bridge, you can then add the URL of the running bridge to your Chainlink node in the UI. - -1. Login to your Chainlink node -2. Click "Bridges" -3. Add a new bridge -4. Enter your bridges URL, for example: `http://localhost:8080/` - -If your bridge has multiple paths or specifies a path other than `/`, you'll need to take that into account when adding -your bridge in Chainlink. For example, with the [RapidAPI](json/rapidapi.json) example, you'd have two URLs: - -- `http://localhost:8080/get` -- `http://localhost:8080/post` - -## Bridge JSON - -Example JSON file below with all the fields set: -```json -[ - { - "name": "Example", - "method": "POST", - "url": "http://exampleapi.com/endpoint", - "path": "/", - "auth": { - "type": "header", - "key": "X-API-KEY", - "env": "API_KEY" - }, - "opts": { - "queryPassthrough": false, - "query": { - "key": "value" - }, - "body": "{\"message\":\"Hello\"}", - "expectedCode": 200 - } - } -] -``` +func (gs *GasStation) Opts() *bridges.Opts { + return &bridges.Opts{ + Name: "GasStation", + Lambda: true, + } +} -To then use this save it to file, for example `bridge.json`, then run: -``` -bridges -b bridge.json +func main() { + bridges.NewServer(&GasStation{}).Start(8080) +} ``` -The resulting adaptor will perform the following API call, when called on `POST http://localhost:8080/`: - -- HTTP Method: `POST` -- Header X-API-KEY: From environment variable `API_KEY` -- URL: `http://exampleapi.com/endpoint?key=value` -- Body: `{"message":"Hello"}` - -It will then check to see if the status code returned was 200. - -## Examples - -JSON: - -- [CryptoCompare](json/cryptocompare.json): Simplest example. -- [AlphaVantage](json/alphavantage.json): Uses GET param authentication, param passthrough. -- [RapidAPI](json/rapidapi.json): Two adaptors specified, header authentication and param & form passthrough. - -Interface implementations: - -- [CryptoCompare](examples/cryptocompare): Simplest example. -- [API Aggregator](examples/apiaggregator): Aggregates multiple endpoints using mean/median/mode. -- [Wolfram Alpha](examples/wolframalpha): Short answers API, non-JSON, uses string splitting. -- [Asset Price](https://github.com/linkpoolio/asset-price-cl-ea): A more complex real example that aggregates crypto asset prices from multiple exchanges by weighted volume. - -## Implement your own +### Authenticated HTTP Calls +Bridges also provides an interface to support authentication methods when making HTTP requests to external sources. By +default, bridges supports authentication via HTTP headers or GET parameters. +Below is a modified version of the WolframAlpha adapter, showing authentication setting the `appid` header from the +`APP_ID` environment variable: ```go package main import ( - "github.com/linkpoolio/bridges/bridge" + "errors" + "fmt" + "github.com/linkpoolio/bridges" + "net/http" + "os" + "strings" ) -type MyAdaptor struct{} - -func (ma *MyAdaptor) Run(h *bridge.Helper) (interface{}, error) { - return map[string]string{"hello": "world"}, nil +type WolframAlpha struct{} + +func (cc *WolframAlpha) Run(h *bridges.Helper) (interface{}, error) { + b, err := h.HTTPCallRawWithOpts( + http.MethodGet, + "https://api.wolframalpha.com/v1/result", + bridges.CallOpts{ + Auth: bridges.NewAuth(bridges.AuthParam, "appid", os.Getenv("APP_ID")), + Query: map[string]interface{}{ + "i": h.GetParam("query"), + }, + }, + ) + return fmt.Sprint(b), err } -func (ma *MyAdaptor) Opts() *bridge.Opts { - return &bridge.Opts{ - Name: "MyAdaptor", +func (cc *WolframAlpha) Opts() *bridges.Opts { + return &bridges.Opts{ + Name: "WolframAlpha", Lambda: true, } } func main() { - bridge.NewServer(&MyAdaptor{}).Start(8080) + bridges.NewServer(&WolframAlpha{}).Start(8080) } ``` -### TODO - -- [ ] Increase test coverage -- [ ] Support S3 urls for adaptor fetching -- [ ] Look at the validity of doing a Docker Hub style adaptor repository - ### Contributing We welcome all contributors, please raise any issues for any feature request, issue or suggestion you may have. diff --git a/bridge/bridge.go b/bridge.go similarity index 99% rename from bridge/bridge.go rename to bridge.go index 898a673..b067288 100755 --- a/bridge/bridge.go +++ b/bridge.go @@ -1,4 +1,4 @@ -package bridge +package bridges import ( "bytes" @@ -389,4 +389,4 @@ type Header struct { // Authenticate takes the key and value given and sets it as a header func (p *Header) Authenticate(r *http.Request) { r.Header.Add(p.Key, p.Value) -} +} \ No newline at end of file diff --git a/bridge/bridge_test.go b/bridge_test.go similarity index 99% rename from bridge/bridge_test.go rename to bridge_test.go index fa8c465..6aee3e7 100755 --- a/bridge/bridge_test.go +++ b/bridge_test.go @@ -1,4 +1,4 @@ -package bridge +package bridges import ( "bytes" diff --git a/examples/apiaggregator/main.go b/examples/apiaggregator/main.go index e223e5c..9836b07 100755 --- a/examples/apiaggregator/main.go +++ b/examples/apiaggregator/main.go @@ -3,7 +3,7 @@ package main import ( "errors" "fmt" - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "github.com/montanaflynn/stats" "github.com/oliveagle/jsonpath" "net/http" @@ -41,7 +41,7 @@ type Result struct { type APIAggregator struct{} // Run is the bridge.Bridge Run implementation that returns the aggregated result -func (cc *APIAggregator) Run(h *bridge.Helper) (interface{}, error) { +func (cc *APIAggregator) Run(h *bridges.Helper) (interface{}, error) { al := len(h.Data.Get("api").Array()) pl := len(h.Data.Get("paths").Array()) @@ -84,19 +84,19 @@ func (cc *APIAggregator) Run(h *bridge.Helper) (interface{}, error) { } // Opts is the bridge.Bridge implementation -func (cc *APIAggregator) Opts() *bridge.Opts { - return &bridge.Opts{ +func (cc *APIAggregator) Opts() *bridges.Opts { + return &bridges.Opts{ Name: "APIAggregator", Lambda: true, } } func main() { - bridge.NewServer(&APIAggregator{}).Start(8080) + bridges.NewServer(&APIAggregator{}).Start(8080) } func performRequest( - h *bridge.Helper, + h *bridges.Helper, wg *sync.WaitGroup, api string, path string, diff --git a/examples/apiaggregator/main_test.go b/examples/apiaggregator/main_test.go index 7254e16..7274aa1 100755 --- a/examples/apiaggregator/main_test.go +++ b/examples/apiaggregator/main_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "github.com/stretchr/testify/assert" "testing" ) @@ -25,10 +25,10 @@ func TestAPIAggregator_Run(t *testing.T) { "type": at, } - json, err := bridge.ParseInterface(p) + json, err := bridges.ParseInterface(p) assert.Nil(t, err) - h := bridge.NewHelper(json) + h := bridges.NewHelper(json) obj, err := aa.Run(h) assert.Nil(t, err) @@ -45,10 +45,10 @@ func TestAPIAggregator_Run(t *testing.T) { func TestFetch_EmptyParam(t *testing.T) { p := map[string]interface{}{} aa := APIAggregator{} - json, err := bridge.ParseInterface(p) + json, err := bridges.ParseInterface(p) assert.Nil(t, err) - h := bridge.NewHelper(json) + h := bridges.NewHelper(json) _, err = aa.Run(h) assert.Equal(t, err.Error(), "Invalid api and path array") @@ -66,10 +66,10 @@ func TestFetch_InvalidArray(t *testing.T) { "type": "mode", } aa := APIAggregator{} - json, err := bridge.ParseInterface(p) + json, err := bridges.ParseInterface(p) assert.Nil(t, err) - h := bridge.NewHelper(json) + h := bridges.NewHelper(json) _, err = aa.Run(h) assert.Equal(t, err.Error(), "Invalid api and path array") diff --git a/examples/cryptocompare/main.go b/examples/cryptocompare/main.go index aa9f1a8..aa57be6 100755 --- a/examples/cryptocompare/main.go +++ b/examples/cryptocompare/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "net/http" ) @@ -10,7 +10,7 @@ import ( type CryptoCompare struct{} // Run is the bridge.Bridge Run implementation that returns the price response -func (cc *CryptoCompare) Run(h *bridge.Helper) (interface{}, error) { +func (cc *CryptoCompare) Run(h *bridges.Helper) (interface{}, error) { r := make(map[string]interface{}) err := h.HTTPCall( http.MethodGet, @@ -21,13 +21,13 @@ func (cc *CryptoCompare) Run(h *bridge.Helper) (interface{}, error) { } // Opts is the bridge.Bridge implementation -func (cc *CryptoCompare) Opts() *bridge.Opts { - return &bridge.Opts{ +func (cc *CryptoCompare) Opts() *bridges.Opts { + return &bridges.Opts{ Name: "CryptoCompare", Lambda: true, } } func main() { - bridge.NewServer(&CryptoCompare{}).Start(8080) + bridges.NewServer(&CryptoCompare{}).Start(8080) } diff --git a/examples/cryptocompare/main_test.go b/examples/cryptocompare/main_test.go index 9781995..8e41a8a 100755 --- a/examples/cryptocompare/main_test.go +++ b/examples/cryptocompare/main_test.go @@ -1,14 +1,14 @@ package main import ( - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "github.com/stretchr/testify/assert" "testing" ) func TestCryptoCompare_Run(t *testing.T) { cc := CryptoCompare{} - obj, err := cc.Run(bridge.NewHelper(nil)) + obj, err := cc.Run(bridges.NewHelper(nil)) assert.Nil(t, err) resp, ok := obj.(map[string]interface{}) diff --git a/examples/gasstation/main.go b/examples/gasstation/main.go index 1d647f5..bdd978d 100755 --- a/examples/gasstation/main.go +++ b/examples/gasstation/main.go @@ -1,14 +1,14 @@ package main import ( - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "net/http" ) type GasStation struct{} // Run implements Bridge Run for querying the Wolfram short answers API -func (gs *GasStation) Run(h *bridge.Helper) (interface{}, error) { +func (gs *GasStation) Run(h *bridges.Helper) (interface{}, error) { obj := make(map[string]interface{}) err := h.HTTPCall( http.MethodGet, @@ -19,13 +19,17 @@ func (gs *GasStation) Run(h *bridge.Helper) (interface{}, error) { } // Opts is the bridge.Bridge implementation -func (gs *GasStation) Opts() *bridge.Opts { - return &bridge.Opts{ +func (gs *GasStation) Opts() *bridges.Opts { + return &bridges.Opts{ Name: "GasStation", Lambda: true, } } +func Handler(w http.ResponseWriter, r *http.Request) { + bridges.NewServer(&GasStation{}).Handler(w, r) +} + func main() { - bridge.NewServer(&GasStation{}).Start(8080) + bridges.NewServer(&GasStation{}).Start(8080) } diff --git a/examples/gasstation/main_test.go b/examples/gasstation/main_test.go index 02c9d9c..f52da97 100755 --- a/examples/gasstation/main_test.go +++ b/examples/gasstation/main_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "github.com/stretchr/testify/assert" "testing" ) @@ -9,7 +9,7 @@ import ( func TestGasStation_Run(t *testing.T) { wa := GasStation{} - h := bridge.NewHelper(&bridge.JSON{}) + h := bridges.NewHelper(&bridges.JSON{}) val, err := wa.Run(h) avg, ok := val.(float64) @@ -24,3 +24,4 @@ func TestGasStation_Opts(t *testing.T) { assert.Equal(t, opts.Name, "GasStation") assert.True(t, opts.Lambda) } + diff --git a/examples/wolframalpha/main.go b/examples/wolframalpha/main.go index 9003876..7c4ebc8 100755 --- a/examples/wolframalpha/main.go +++ b/examples/wolframalpha/main.go @@ -2,7 +2,7 @@ package main import ( "errors" - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "net/http" "os" "strings" @@ -29,12 +29,12 @@ import ( type WolframAlpha struct{} // Run implements Bridge Run for querying the Wolfram short answers API -func (cc *WolframAlpha) Run(h *bridge.Helper) (interface{}, error) { +func (cc *WolframAlpha) Run(h *bridges.Helper) (interface{}, error) { b, err := h.HTTPCallRawWithOpts( http.MethodGet, "https://api.wolframalpha.com/v1/result", - bridge.CallOpts{ - Auth: bridge.NewAuth(bridge.AuthParam, "appid", os.Getenv("APP_ID")), + bridges.CallOpts{ + Auth: bridges.NewAuth(bridges.AuthParam, "appid", os.Getenv("APP_ID")), Query: map[string]interface{}{ "i": h.GetParam("query"), }, @@ -49,13 +49,13 @@ func (cc *WolframAlpha) Run(h *bridge.Helper) (interface{}, error) { } // Opts is the bridge.Bridge implementation -func (cc *WolframAlpha) Opts() *bridge.Opts { - return &bridge.Opts{ +func (cc *WolframAlpha) Opts() *bridges.Opts { + return &bridges.Opts{ Name: "WolframAlpha", Lambda: true, } } func main() { - bridge.NewServer(&WolframAlpha{}).Start(8080) + bridges.NewServer(&WolframAlpha{}).Start(8080) } diff --git a/examples/wolframalpha/main_test.go b/examples/wolframalpha/main_test.go index 12ab6dd..93982d2 100755 --- a/examples/wolframalpha/main_test.go +++ b/examples/wolframalpha/main_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/linkpoolio/bridges/bridge" + "github.com/linkpoolio/bridges" "github.com/stretchr/testify/assert" "testing" ) @@ -25,10 +25,10 @@ func TestWolframAlpha_Run(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - json, err := bridge.ParseInterface(c.data) + json, err := bridges.ParseInterface(c.data) assert.Nil(t, err) - h := bridge.NewHelper(json) + h := bridges.NewHelper(json) _, err = wa.Run(h) assert.Equal(t, c.error, err.Error()) diff --git a/json/alphavantage.json b/json/alphavantage.json deleted file mode 100755 index 4c429d2..0000000 --- a/json/alphavantage.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "name": "AlphaVantage", - "method": "GET", - "url": "https://www.alphavantage.co/query", - "auth": { - "type": "param", - "key": "apikey", - "env": "API_KEY" - }, - "opts": { - "queryPassthrough": true - } - } -] diff --git a/json/cryptocompare.json b/json/cryptocompare.json deleted file mode 100755 index a4026aa..0000000 --- a/json/cryptocompare.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "name": "CryptoCompare", - "method": "GET", - "url": "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,JPY,EUR" - } -] \ No newline at end of file diff --git a/json/invalid.json b/json/invalid.json deleted file mode 100755 index 81750b9..0000000 --- a/json/invalid.json +++ /dev/null @@ -1 +0,0 @@ -{ \ No newline at end of file diff --git a/json/placeholder.json b/json/placeholder.json deleted file mode 100755 index 6783051..0000000 --- a/json/placeholder.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "name": "Placeholder", - "method": "GET", - "url": "https://reqres.in/api/users", - "opts": { - "query": { - "page": "page" - } - } - } -] \ No newline at end of file diff --git a/json/rapidapi.json b/json/rapidapi.json deleted file mode 100755 index 7bc21ac..0000000 --- a/json/rapidapi.json +++ /dev/null @@ -1,28 +0,0 @@ -[ - { - "name": "RapidAPI GET", - "method": "GET", - "path": "/get", - "auth": { - "type": "header", - "key": "X-RapidAPI-Key", - "env": "API_KEY" - }, - "opts": { - "queryPassthrough": true - } - }, - { - "name": "RapidAPI POST", - "method": "POST", - "path": "/post", - "auth": { - "type": "header", - "key": "X-RapidAPI-Key", - "env": "API_KEY" - }, - "opts": { - "queryPassthrough": true - } - } -] \ No newline at end of file diff --git a/main.go b/main.go deleted file mode 100755 index c6d8f3b..0000000 --- a/main.go +++ /dev/null @@ -1,125 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/linkpoolio/bridges/bridge" - "github.com/sirupsen/logrus" - "github.com/spf13/pflag" - "github.com/utahta/go-openuri" - "io/ioutil" - "net/http" - "os" -) - -// Bridge is the struct to represent the bridge JSON -// files to used as bridges -type Bridge struct { - Name string `json:"name"` - Method string `json:"method"` - URL string `json:"url"` - Path string `json:"path"` - Auth BridgeCallAuth `json:"auth"` - Opts bridge.CallOpts `json:"opts"` -} - -// BridgeCallAuth represents the type of authentication to be used -// on the call to the bridges API -type BridgeCallAuth struct { - Type string `json:"type"` - Key string `json:"key"` - Env string `json:"env"` -} - -// JSON is the Bridge implementation that is used for the bridges -// cli to start bridge -type JSON struct { - bridge Bridge -} - -// NewJSONBridges parses the uri and returns an array of initialised -// bridges based from the JSON body of the given URI. -func NewJSONBridges(uri string) ([]bridge.Bridge, error) { - var bs []Bridge - var js []bridge.Bridge - if len(uri) == 0 { - return nil, errors.New("Empty bridge URI given") - } else if o, err := openuri.Open(uri); err != nil { - return nil, err - } else if b, err := ioutil.ReadAll(o); err != nil { - return nil, err - } else if err := json.Unmarshal(b, &bs); err != nil { - return nil, err - } - for _, a := range bs { - a.Opts.Auth = bridge.NewAuth( - a.Auth.Type, - a.Auth.Key, - os.Getenv(a.Auth.Env), - ) - js = append(js, &JSON{a}) - } - return js, nil -} - -// Run is the Bridge implementation which takes the JSON version of an adaptor -// makes a call based on whats defined in the model, and returns the response -func (ja *JSON) Run(h *bridge.Helper) (interface{}, error) { - r := make(map[string]interface{}) - p := make(map[string]interface{}) - for k, v := range ja.bridge.Opts.Query { - p[k] = h.GetParam(fmt.Sprintf("%s", v)) - } - ja.bridge.Opts.Query = p - var url string - if len(ja.bridge.URL) == 0 { - url = h.GetParam("url") - } else { - url = ja.bridge.URL - } - err := h.HTTPCallWithOpts(ja.bridge.Method, url, &r, ja.bridge.Opts) - return r, err -} - -// Opts returns a bridge options type that has values set -// based on the given JSON file -func (ja *JSON) Opts() *bridge.Opts { - return &bridge.Opts{ - Name: ja.bridge.Name, - Path: ja.bridge.Path, - Lambda: true, - } -} - -// Handler is the entrypoint for GCP functions -func Handler(w http.ResponseWriter, r *http.Request) { - env := os.Getenv("BRIDGE") - if len(env) == 0 { - w.Write([]byte("No bridge set")) - return - } else if b, err := NewJSONBridges(env); err != nil { - w.Write([]byte(err.Error())) - return - } else { - bridge.NewServer(b...).Handler(w, r) - } -} - -func main() { - var uri string - var port int - pflag.StringVarP(&uri, "bridge", "b", "", "Filepath/URL of bridge JSON file") - pflag.IntVarP(&port, "port", "p", 8080, "Server port") - pflag.Parse() - - env := os.Getenv("BRIDGE") - if len(uri) == 0 && len(env) != 0 { - uri = env - } - if b, err := NewJSONBridges(uri); err != nil { - logrus.Fatalf("Failed to load bridge: %v", err) - } else { - bridge.NewServer(b...).Start(port) - } -} diff --git a/main_test.go b/main_test.go deleted file mode 100755 index db34abd..0000000 --- a/main_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "github.com/linkpoolio/bridges/bridge" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "testing" -) - -func TestNewJSONBridges(t *testing.T) { - cases := []struct { - path string - name string - req map[string]interface{} - expectedKeys []string - }{ - {"json/cryptocompare.json", "CryptoCompare", nil, []string{"EUR", "JPY", "USD"}}, - {"json/alphavantage.json", "AlphaVantage", map[string]interface{}{ - "function": "CURRENCY_EXCHANGE_RATE", - "from_currency": "GBP", - "to_currency": "EUR", - }, []string{"Error Message"}}, - {"https://s3.linkpool.io/bridges/cryptocompare.json", "CryptoCompare", nil, []string{"EUR", "JPY", "USD"}}, - {"https://s3.linkpool.io/bridges/alphavantage.json", "AlphaVantage", map[string]interface{}{ - "function": "CURRENCY_EXCHANGE_RATE", - "from_currency": "GBP", - "to_currency": "EUR", - }, []string{"Error Message"}}, - {"json/placeholder.json", "Placeholder", map[string]interface{}{ - "page": 1, - }, []string{"page", "per_page", "total_pages"}}, - } - for _, c := range cases { - t.Run(c.path, func(t *testing.T) { - bs, err := NewJSONBridges(c.path) - assert.Nil(t, err) - assert.Len(t, bs, 1) - - for _, b := range bs { - assert.Equal(t, c.name, b.Opts().Name) - assert.True(t, b.Opts().Lambda) - - json, err := bridge.ParseInterface(c.req) - assert.Nil(t, err) - h := bridge.NewHelper(json) - - obj, err := b.Run(h) - assert.NotNil(t, obj) - assert.Nil(t, err) - - resMap, ok := obj.(map[string]interface{}) - assert.True(t, ok) - - for _, k := range c.expectedKeys { - _, ok := resMap[k] - assert.True(t, ok) - } - } - }) - } -} - -func TestNewJSONBridges_Errors(t *testing.T) { - _, err := NewJSONBridges("") - assert.Equal(t, "Empty bridge URI given", err.Error()) - - _, err = NewJSONBridges("json/invalid.json") - assert.Equal(t, "unexpected end of JSON input", err.Error()) - - _, err = NewJSONBridges("http://invalidqwerty.com") - assert.Contains(t, err.Error(), "no such host") -} - -func TestHandler(t *testing.T) { - p := map[string]interface{}{ - "id": "1234", - } - pb, err := json.Marshal(p) - assert.Nil(t, err) - - req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(pb)) - assert.Nil(t, err) - rr := httptest.NewRecorder() - - err = os.Setenv("BRIDGE", "json/cryptocompare.json") - assert.Nil(t, err) - Handler(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - err = os.Unsetenv("BRIDGE") - assert.Nil(t, err) - - body, err := ioutil.ReadAll(rr.Body) - assert.Nil(t, err) - json, err := bridge.Parse(body) - assert.Nil(t, err) - - assert.Equal(t, "1234", json.Get("jobRunId").String()) - - data := json.Get("data").Map() - _, ok := data["USD"] - assert.True(t, ok) - _, ok = data["JPY"] - assert.True(t, ok) - _, ok = data["EUR"] - assert.True(t, ok) -} - -func TestHandler_NilBridge(t *testing.T) { - p := map[string]interface{}{ - "id": "1234", - } - pb, err := json.Marshal(p) - assert.Nil(t, err) - - req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(pb)) - assert.Nil(t, err) - rr := httptest.NewRecorder() - - err = os.Setenv("BRIDGE", "") - assert.Nil(t, err) - Handler(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - err = os.Unsetenv("BRIDGE") - assert.Nil(t, err) - - body, err := ioutil.ReadAll(rr.Body) - assert.Nil(t, err) - assert.Equal(t, "No bridge set", string(body)) -} - -func TestHandler_InvalidBridge(t *testing.T) { - p := map[string]interface{}{ - "id": "1234", - } - pb, err := json.Marshal(p) - assert.Nil(t, err) - - req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(pb)) - assert.Nil(t, err) - rr := httptest.NewRecorder() - - err = os.Setenv("BRIDGE", "json/invalid.json") - assert.Nil(t, err) - Handler(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - err = os.Unsetenv("BRIDGE") - assert.Nil(t, err) - - body, err := ioutil.ReadAll(rr.Body) - assert.Nil(t, err) - assert.Equal(t, "unexpected end of JSON input", string(body)) -}