Skip to content

Commit

Permalink
Merge pull request #9 from stevenferrer/feature/config-api
Browse files Browse the repository at this point in the history
Implement config API client
  • Loading branch information
stevenferrer authored Jun 28, 2020
2 parents 971697d + 599979a commit 9249764
Show file tree
Hide file tree
Showing 15 changed files with 1,013 additions and 14 deletions.
17 changes: 3 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,17 @@ func main() {

- [Solr-Go](#solr-go)
- [Contents](#contents)
- [Goals](#goals)
- [Notes](#notes)
- [Installation](#installation)
- [Usage](#usage)
- [Features](#features)
- [Contributing](#contributing)


## Goals

The goal of this project is to support the majority of operations in Solr via API.

* Basic operations: querying, indexing, auto-suggest etc.
* Admin operations:
* [Schema API](https://lucene.apache.org/solr/guide/8_5/schema-api.html)
* [Config API](https://lucene.apache.org/solr/guide/8_5/config-api.html)
* [Configset API](https://lucene.apache.org/solr/guide/8_5/configsets-api.html)

## Notes

* This is a *WORK IN-PROGRESS*, API might change a lot before *v1*
* I'm currently using my project as the testbed for this module
* Tested on [Solr 8.5](https://lucene.apache.org/solr/guide/8_5/)
* Tested using [Solr 8.5](https://lucene.apache.org/solr/guide/8_5/)

## Installation

Expand Down Expand Up @@ -93,9 +81,10 @@ A detailed documentation shall follow after *v1*. For now you can start looking
- [ ] Example
- [x] Suggester client
- [x] Unified solr client
- [x] Config API client
- [ ] Collections API client
- [ ] Configset API client
- [ ] Config API client
- [ ] SolrCloud support (V2 API)
- [ ] Basic auth support
- [ ] Documentation

Expand Down
137 changes: 137 additions & 0 deletions config/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package config

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/pkg/errors"
)

// Client is a config API client
type Client interface {
GetConfig(ctx context.Context, collection string) (*Response, error)
SendCommands(ctx context.Context, collection string, commands ...Commander) error
}

type client struct {
host string
port int
proto string
httpClient *http.Client
}

// New is a factory for config client
func New(host string, port int) Client {
proto := "http"
return &client{
host: host,
port: port,
proto: proto,
httpClient: &http.Client{
Timeout: time.Second * 60,
},
}
}

// NewWithHTTPClient is a factory for config client
func NewWithHTTPClient(host string, port int, httpClient *http.Client) Client {
proto := "http"
return &client{
host: host,
port: port,
proto: proto,
httpClient: httpClient,
}
}

func (c *client) GetConfig(ctx context.Context, collection string) (*Response, error) {
theURL, err := c.buildURL(collection)
if err != nil {
return nil, errors.Wrap(err, "build url")
}

httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, theURL.String(), nil)
if err != nil {
return nil, errors.Wrap(err, "new http request")
}

return c.do(httpReq)
}

func (c *client) SendCommands(ctx context.Context, collection string, commands ...Commander) error {
if len(commands) == 0 {
return nil
}

theURL, err := c.buildURL(collection)
if err != nil {
return errors.Wrap(err, "build url")
}

// build commands
commandStrs := []string{}
for _, command := range commands {
commandStr, err := command.Command()
if err != nil {
return errors.Wrap(err, "build commad")
}

commandStrs = append(commandStrs, commandStr)
}

// send commands to solr
requestBody := "{" + strings.Join(commandStrs, ",") + "}"

return c.sendCommands(ctx, theURL.String(), []byte(requestBody))
}

func (c *client) buildURL(collection string) (*url.URL, error) {
u, err := url.Parse(fmt.Sprintf("%s://%s:%d/solr/%s/config",
c.proto, c.host, c.port, collection))
if err != nil {
return nil, errors.Wrap(err, "parse url")
}

return u, nil
}

func (c *client) sendCommands(ctx context.Context, urlStr string, body []byte) error {
httpReq, err := http.NewRequestWithContext(ctx,
http.MethodPost, urlStr, bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "new http request")
}

_, err = c.do(httpReq)
if err != nil {
return errors.Wrap(err, "send commands")
}

return err
}

func (c *client) do(httpReq *http.Request) (*Response, error) {
httpReq.Header.Add("content-type", "application/json")
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, errors.Wrap(err, "http do request")
}

var resp Response
err = json.NewDecoder(httpResp.Body).Decode(&resp)
if err != nil {
return nil, errors.Wrap(err, "decode response")
}

if httpResp.StatusCode > http.StatusOK {
return nil, resp.Error
}

return &resp, nil
}
124 changes: 124 additions & 0 deletions config/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package config_test

import (
"context"
"net/http"
"testing"
"time"

"github.com/dnaeon/go-vcr/recorder"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

solrconfig "github.com/stevenferrer/solr-go/config"
)

func TestClient(t *testing.T) {
ctx := context.Background()
collection := "gettingstarted"
host := "localhost"
port := 8983
timeout := time.Second * 6

// only for covering
_ = solrconfig.New(host, port)

t.Run("retrieve config", func(t *testing.T) {
rec, err := recorder.New("fixtures/retrieve-config")
require.NoError(t, err)
defer rec.Stop()

configClient := solrconfig.NewWithHTTPClient(host, port, &http.Client{
Timeout: timeout,
Transport: rec,
})

resp, err := configClient.GetConfig(ctx, collection)
require.NoError(t, err)

assert.NotNil(t, resp)
})

t.Run("send commands", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
rec, err := recorder.New("fixtures/send-commands-ok")
require.NoError(t, err)
defer rec.Stop()

configClient := solrconfig.NewWithHTTPClient(host, port, &http.Client{
Timeout: timeout,
Transport: rec,
})

setUpdateHandlerAutoCommit := solrconfig.NewSetPropCommand(
"updateHandler.autoCommit.maxTime", 15000)

addSuggestComponent := solrconfig.NewComponentCommand(
solrconfig.AddSearchComponent,
map[string]interface{}{
"name": "suggest",
"class": "solr.SuggestComponent",
"suggester": map[string]string{
"name": "mySuggester",
"lookupImpl": "FuzzyLookupFactory",
"dictionaryImpl": "DocumentDictionaryFactory",
"field": "_text_",
"suggestAnalyzerFieldType": "text_general",
},
},
)

addSuggestHandler := solrconfig.NewComponentCommand(
solrconfig.AddRequestHandler,
map[string]interface{}{
"name": "/suggest",
"class": "solr.SearchHandler",
"startup": "lazy",
"defaults": map[string]interface{}{
"suggest": true,
"suggest.count": 10,
"suggest.dictionary": "mySuggester",
},
"components": []string{"suggest"},
},
)

err = configClient.SendCommands(ctx, collection,
setUpdateHandlerAutoCommit,
addSuggestComponent,
addSuggestHandler,
)
assert.NoError(t, err)
})

t.Run("error", func(t *testing.T) {
rec, err := recorder.New("fixtures/send-commands-error")
require.NoError(t, err)
defer rec.Stop()

configClient := solrconfig.NewWithHTTPClient(host, port, &http.Client{
Timeout: timeout,
Transport: rec,
})

addSuggestComponent := solrconfig.NewComponentCommand(
solrconfig.AddSearchComponent,
map[string]interface{}{
"name": "suggest",
"class": "solr.SuggestComponent",
"suggester": map[string]string{
"name": "mySuggester",
"lookupImpl": "FuzzyLookupFactory-BLAH-BLAH",
"dictionaryImpl": "DocumentDictionaryFactory-BLAH-BLAH",
"field": "_text_",
"suggestAnalyzerFieldType": "text_general",
},
},
)

err = configClient.SendCommands(ctx, collection, addSuggestComponent)
assert.Error(t, err)
})
})

}
Loading

0 comments on commit 9249764

Please sign in to comment.