Skip to content

Commit 9249764

Browse files
authored
Merge pull request #9 from stevenferrer/feature/config-api
Implement config API client
2 parents 971697d + 599979a commit 9249764

File tree

15 files changed

+1013
-14
lines changed

15 files changed

+1013
-14
lines changed

README.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,17 @@ func main() {
3434

3535
- [Solr-Go](#solr-go)
3636
- [Contents](#contents)
37-
- [Goals](#goals)
3837
- [Notes](#notes)
3938
- [Installation](#installation)
4039
- [Usage](#usage)
4140
- [Features](#features)
4241
- [Contributing](#contributing)
4342

44-
45-
## Goals
46-
47-
The goal of this project is to support the majority of operations in Solr via API.
48-
49-
* Basic operations: querying, indexing, auto-suggest etc.
50-
* Admin operations:
51-
* [Schema API](https://lucene.apache.org/solr/guide/8_5/schema-api.html)
52-
* [Config API](https://lucene.apache.org/solr/guide/8_5/config-api.html)
53-
* [Configset API](https://lucene.apache.org/solr/guide/8_5/configsets-api.html)
54-
5543
## Notes
5644

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

6149
## Installation
6250

@@ -93,9 +81,10 @@ A detailed documentation shall follow after *v1*. For now you can start looking
9381
- [ ] Example
9482
- [x] Suggester client
9583
- [x] Unified solr client
84+
- [x] Config API client
9685
- [ ] Collections API client
9786
- [ ] Configset API client
98-
- [ ] Config API client
87+
- [ ] SolrCloud support (V2 API)
9988
- [ ] Basic auth support
10089
- [ ] Documentation
10190

config/client.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package config
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
"net/url"
10+
"strings"
11+
"time"
12+
13+
"github.com/pkg/errors"
14+
)
15+
16+
// Client is a config API client
17+
type Client interface {
18+
GetConfig(ctx context.Context, collection string) (*Response, error)
19+
SendCommands(ctx context.Context, collection string, commands ...Commander) error
20+
}
21+
22+
type client struct {
23+
host string
24+
port int
25+
proto string
26+
httpClient *http.Client
27+
}
28+
29+
// New is a factory for config client
30+
func New(host string, port int) Client {
31+
proto := "http"
32+
return &client{
33+
host: host,
34+
port: port,
35+
proto: proto,
36+
httpClient: &http.Client{
37+
Timeout: time.Second * 60,
38+
},
39+
}
40+
}
41+
42+
// NewWithHTTPClient is a factory for config client
43+
func NewWithHTTPClient(host string, port int, httpClient *http.Client) Client {
44+
proto := "http"
45+
return &client{
46+
host: host,
47+
port: port,
48+
proto: proto,
49+
httpClient: httpClient,
50+
}
51+
}
52+
53+
func (c *client) GetConfig(ctx context.Context, collection string) (*Response, error) {
54+
theURL, err := c.buildURL(collection)
55+
if err != nil {
56+
return nil, errors.Wrap(err, "build url")
57+
}
58+
59+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, theURL.String(), nil)
60+
if err != nil {
61+
return nil, errors.Wrap(err, "new http request")
62+
}
63+
64+
return c.do(httpReq)
65+
}
66+
67+
func (c *client) SendCommands(ctx context.Context, collection string, commands ...Commander) error {
68+
if len(commands) == 0 {
69+
return nil
70+
}
71+
72+
theURL, err := c.buildURL(collection)
73+
if err != nil {
74+
return errors.Wrap(err, "build url")
75+
}
76+
77+
// build commands
78+
commandStrs := []string{}
79+
for _, command := range commands {
80+
commandStr, err := command.Command()
81+
if err != nil {
82+
return errors.Wrap(err, "build commad")
83+
}
84+
85+
commandStrs = append(commandStrs, commandStr)
86+
}
87+
88+
// send commands to solr
89+
requestBody := "{" + strings.Join(commandStrs, ",") + "}"
90+
91+
return c.sendCommands(ctx, theURL.String(), []byte(requestBody))
92+
}
93+
94+
func (c *client) buildURL(collection string) (*url.URL, error) {
95+
u, err := url.Parse(fmt.Sprintf("%s://%s:%d/solr/%s/config",
96+
c.proto, c.host, c.port, collection))
97+
if err != nil {
98+
return nil, errors.Wrap(err, "parse url")
99+
}
100+
101+
return u, nil
102+
}
103+
104+
func (c *client) sendCommands(ctx context.Context, urlStr string, body []byte) error {
105+
httpReq, err := http.NewRequestWithContext(ctx,
106+
http.MethodPost, urlStr, bytes.NewReader(body))
107+
if err != nil {
108+
return errors.Wrap(err, "new http request")
109+
}
110+
111+
_, err = c.do(httpReq)
112+
if err != nil {
113+
return errors.Wrap(err, "send commands")
114+
}
115+
116+
return err
117+
}
118+
119+
func (c *client) do(httpReq *http.Request) (*Response, error) {
120+
httpReq.Header.Add("content-type", "application/json")
121+
httpResp, err := c.httpClient.Do(httpReq)
122+
if err != nil {
123+
return nil, errors.Wrap(err, "http do request")
124+
}
125+
126+
var resp Response
127+
err = json.NewDecoder(httpResp.Body).Decode(&resp)
128+
if err != nil {
129+
return nil, errors.Wrap(err, "decode response")
130+
}
131+
132+
if httpResp.StatusCode > http.StatusOK {
133+
return nil, resp.Error
134+
}
135+
136+
return &resp, nil
137+
}

config/client_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package config_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
"time"
8+
9+
"github.com/dnaeon/go-vcr/recorder"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
solrconfig "github.com/stevenferrer/solr-go/config"
14+
)
15+
16+
func TestClient(t *testing.T) {
17+
ctx := context.Background()
18+
collection := "gettingstarted"
19+
host := "localhost"
20+
port := 8983
21+
timeout := time.Second * 6
22+
23+
// only for covering
24+
_ = solrconfig.New(host, port)
25+
26+
t.Run("retrieve config", func(t *testing.T) {
27+
rec, err := recorder.New("fixtures/retrieve-config")
28+
require.NoError(t, err)
29+
defer rec.Stop()
30+
31+
configClient := solrconfig.NewWithHTTPClient(host, port, &http.Client{
32+
Timeout: timeout,
33+
Transport: rec,
34+
})
35+
36+
resp, err := configClient.GetConfig(ctx, collection)
37+
require.NoError(t, err)
38+
39+
assert.NotNil(t, resp)
40+
})
41+
42+
t.Run("send commands", func(t *testing.T) {
43+
t.Run("ok", func(t *testing.T) {
44+
rec, err := recorder.New("fixtures/send-commands-ok")
45+
require.NoError(t, err)
46+
defer rec.Stop()
47+
48+
configClient := solrconfig.NewWithHTTPClient(host, port, &http.Client{
49+
Timeout: timeout,
50+
Transport: rec,
51+
})
52+
53+
setUpdateHandlerAutoCommit := solrconfig.NewSetPropCommand(
54+
"updateHandler.autoCommit.maxTime", 15000)
55+
56+
addSuggestComponent := solrconfig.NewComponentCommand(
57+
solrconfig.AddSearchComponent,
58+
map[string]interface{}{
59+
"name": "suggest",
60+
"class": "solr.SuggestComponent",
61+
"suggester": map[string]string{
62+
"name": "mySuggester",
63+
"lookupImpl": "FuzzyLookupFactory",
64+
"dictionaryImpl": "DocumentDictionaryFactory",
65+
"field": "_text_",
66+
"suggestAnalyzerFieldType": "text_general",
67+
},
68+
},
69+
)
70+
71+
addSuggestHandler := solrconfig.NewComponentCommand(
72+
solrconfig.AddRequestHandler,
73+
map[string]interface{}{
74+
"name": "/suggest",
75+
"class": "solr.SearchHandler",
76+
"startup": "lazy",
77+
"defaults": map[string]interface{}{
78+
"suggest": true,
79+
"suggest.count": 10,
80+
"suggest.dictionary": "mySuggester",
81+
},
82+
"components": []string{"suggest"},
83+
},
84+
)
85+
86+
err = configClient.SendCommands(ctx, collection,
87+
setUpdateHandlerAutoCommit,
88+
addSuggestComponent,
89+
addSuggestHandler,
90+
)
91+
assert.NoError(t, err)
92+
})
93+
94+
t.Run("error", func(t *testing.T) {
95+
rec, err := recorder.New("fixtures/send-commands-error")
96+
require.NoError(t, err)
97+
defer rec.Stop()
98+
99+
configClient := solrconfig.NewWithHTTPClient(host, port, &http.Client{
100+
Timeout: timeout,
101+
Transport: rec,
102+
})
103+
104+
addSuggestComponent := solrconfig.NewComponentCommand(
105+
solrconfig.AddSearchComponent,
106+
map[string]interface{}{
107+
"name": "suggest",
108+
"class": "solr.SuggestComponent",
109+
"suggester": map[string]string{
110+
"name": "mySuggester",
111+
"lookupImpl": "FuzzyLookupFactory-BLAH-BLAH",
112+
"dictionaryImpl": "DocumentDictionaryFactory-BLAH-BLAH",
113+
"field": "_text_",
114+
"suggestAnalyzerFieldType": "text_general",
115+
},
116+
},
117+
)
118+
119+
err = configClient.SendCommands(ctx, collection, addSuggestComponent)
120+
assert.Error(t, err)
121+
})
122+
})
123+
124+
}

0 commit comments

Comments
 (0)