-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PROPOSAL / QUESTION] Is there a plan to improve usability by making the API more idiomatic to the Go ecosystem? #65
Comments
Talking about SearchRequest, we've the following function: func (r SearchRequest) Do(ctx context.Context, transport Transport) (*Response, error) I believe that it'd make more sense to have client.Search with a signature such as the following: func (c *Client) Search(ctx context.Context, params SearchRequestParams) (*SearchResponse, error) and then something along the lines of: type SearchResponse struct {
Took int `json:"_took"`
TimedOut bool `json:"timed_out"`
Shards *SearchResponseShards `json:"shards,omitempty"`
Hits *SearchResponseHits `json:"hits,omitempty"`
}
type SearchResponseShards struct {
Total int
Successful int
Skipped int
Failed int
}
type SearchResponseHits struct {
Total SearchResponseHitsTotal
MaxScore json.Number `json:"max_score"`
Hits []SearchResponseHit `json:"hits,omitempty"`
}
type SearchResponseHitsTotal struct {
Value int
Relation string
}
type SearchResponseHit struct {
Index string `json:"_index"`
Type string `json:"_type"`
ID string `json:"_id"`
Score json.Number `json:"_score"`
Source json.RawMessage `json:"_source"`
} The error returned there could be used as follows: resp, err := client.Search(ctx, params)
if err != nil {
var serr *opensearch.SearchError
if errors.As(err, &serr) {
log.Printf("opensearch error: %s\n", serr.Error.Reason)
return ...
}
// otherwise, not the kind of error we expect...
return ...
}
// success flow |
@henvic Thanks for opening an issue. We will take a look at it and will get back soon. |
@henvic Thanks for detailed proposal. We agree that we should improve the interface, to make API more idiomatic to the Go ecosystem. For first release, our focus is not to make any changes to existing API, hence, users who are migrating from elasticsearch oss to OpenSearch will have less impact. Updating API will break existing users. We should invest on improving code generation (api structs) to be more go friendly (not in first release). We will be prioritizing this effort based on community feedback/ask. |
What do people think about adding the idiomatic options and preserving the older accessors? Besides the carrying cost and the confusion of having more than one way to do things, are there more fundamental incompatibilities with both options existing? |
What is the benefit of keeping both. It will complicate the maintenance and it might confuse users on how to use the lib. There are several ways on how to build the lib. We should discuss how we want the lib to be used and enforce the style. Therefor I suggest that the lib has one way of doing things. Currently users have two options on how to perform request and there is now guide on which is the preferred one. This results in users having different implementation of the lib. Currently users can create a request and execute it with the The current implementation of the What implementation options do I see:
This was a short overview of implementations I can think of. There might be more pro and con arguments for each options we could discuss, but right now option 2 seems the best to me. Therefore I created a branch to display the change and the use by adjusting some tests. Moreover I moved the parameters of the request to a new struct and file so we can separate the parameter parsing from the actual request and also prepare the code to be generated once we implement the code generation. I also de-duplicated the request build and execution to a general function as all request do the same. With this change we might also remove the Elasticsearch license stuff as the code changes. What we might want to discuss is the use of pointers for request. Using pointers is faster and in some cases can shorten the users code, as they can give nil as argument, but we also need to check in most cases if the request is not nil and otherwise return an error. One could also argue that the time benefit of using pointers is negligibly when calling a HTTP API. I would really like to hear @wbeckler @dblock and @VachaShah opinion on this. If you have any questions I am happy to explain things further. |
💯 for simplifying, keeping 1 way of doing things, and deleting everything else I am not a go expert to say which one is best, as a rule of thumb I would say that we want developers to write the least amount of code when using the library. As an example the simplification of error handling was along those lines. |
Agreed that it would be best to make it more idiomatic for Go users. I think it would be great if we could soften the blow. @Jakob3xD the reason to keep both (for a short time or maybe a slow transition) is to give users with dependencies on this client some time to adapt. It would be quite a shock to attempt an update of the client to discover the ways that you had been using it (while funky) had been completely changed version over version. |
The issue I see, we can't make old and new work together without creating a new/different client and duplicating code. Just to clarify, I am talking about rewriting/changing the whole opensearch-api package. We could go even further and rethink the file names as using Without a decision on how we want to struct the lib it make no sense to implement any features as it is not clear how they should look like and where they should be. For the general structure of the client, I suggest the linked diff. More over we should create a plugins package which contains a sub package for each plugin. So for example:
Names can be discussed, but for example naming it just security might cause issue as security is a common name. |
In relation to my listed options and my diff.
This would give the users the option to use the lib very low level by giving nil as struc so we don't try to parse it or easy by parsing the response. |
|
I created a POC on how an implementation could look like. @dblock I think, I cover all your points. But there are two things to discuss. The other thing is the generic and specif request. With my POC users can either call the Do() function of the client to get the generic unparsed response or call the more specific function which returns the parsed struct. If we support the https://github.com/Jakob3xD/opensearch-golang - This is the equivalent to the current opensearch-go repo User code examplepackage main
import (
"context"
"fmt"
security "github.com/jakob3xd/opensearch-go-plugin-security"
"github.com/jakob3xd/opensearch-golang/opensearchapi"
)
func main() {
err := test()
if err != nil {
fmt.Println(err)
}
}
func test() error {
ctx := context.Background()
// security
client, err := security.NewDefaultClient()
if err != nil {
return err
}
userResp, err := client.Users.Get(ctx, &security.UsersGetReq{User: "admin"})
if err != nil {
return err
}
fmt.Println(fmt.Sprintf("$#v", userResp))
// opensearchapi
osClient, err := opensearchapi.NewDefaultClient()
if err != nil {
return err
}
catResp, err := osClient.Cat.Indices(ctx, &opensearchapi.CatIndicesReq{})
if err != nil {
return err
}
fmt.Println(fmt.Sprintf("$#v", catResp))
return nil
} |
Great work! I love the proposed implementation. My big question is why do we need the For a start I would put the plugins code into a subfolder in this client, knowing that one day we'll separate them into their own libraries. I don't think we can effectively maintain a large set of go clients rn. |
Currently users can get the response and they can do whatever they want with it. The Do function would preserve this option. Moreover users can create custom Request and use the Do function to execute it. On the other hand users could also just write a simple Do function them self and use the Do of the root client which is still needed. So imo we could also remove it. |
There is one more thing I am concerned about. 404 responses are treated as error which is okay but there is no simple way for users to accept 404 errors. delResp, err := client.Users.Delete(ctx, &security.UsersDeleteReq{User: "test"})
if err != nil {
var errCheck security.Error
if !errors.As(err, &errCheck) || errCheck.Status != "NOT_FOUND" {
return err
}
} Don´t know if this is okay for us or if we need to find a solution for it. Right now I can't think of a nice one. We could return the status code as an extra return value. |
Let's do this until someone complains. |
@Jakob3xD I don't know how to implement this, but conceptually I think I'd be great to have access to the raw HTTP request and the raw HTTP response, so that I can do both |
I checked back with my co-worker and we both think, that we should keep it as it is in my POC. Normally as a user you just want the parsed struct and the error and not the response as you don't want to process it your self. So I suggest writing an implementation like my POC with a plugins folder. Otherwise we would need to return the Response as a third return argument. |
I'm not saying you should do it, but a clean way to do is to have an Inspect function that would receive it and expose an unimported field. This is awkward because it has hidden magic, but at least you keep a sane API for the average consumer and make it hard to abuse it. I'd find it quite useful for debugging or in the very unlikely case where you need to access something that isn't exposed, though. |
@henvic thank your for the reply. If i understand you correctly, then I like your idea. Just to verify. You meant that our response types should have an unexported response field with a So for example:
If this is your suggestion, I have one question. Would you expect the UsersGetResp to be returned on errors or should it be nil? For error debugging it would make sense to return it?
|
I like |
I added an Inspect struct to opensearch and used it for the security responses in my POC branches. I assume you meant the http request and not the lib requests which the user created? Not sure though if we need/want the request. |
I am thinking at a high level: a user may find a workaround for features not present in the library if they get access to the original request and response. |
If you want the request, then we could just replace all the |
Took me some time to build the basic structure due to limited time but it is currently present under my feature branch. I appreciate if someone can take a look at the current state and tell me if I need to change something or something is unclear. |
@Jakob3xD This is great work. I think you should hesitate less and go for it. |
Following is a list of functions that already exist in the current client. They are checked when I implemented them in my feature branch. The lists also helps me to keep track of what I am missing. List
The diff is already huge: main...opensearch-project:opensearch-go:feature/idiomatic |
@Jakob3xD do you need help with this? |
If you want, you can use my feature branch as example/base for an ism or security client. |
@dblock The features branch now contains nearly all functions. How should we proceed "merging" this? There might be some comments that need to be updated due to copy paste and the guids also need an update. |
Make a passing PR with a major version increment? |
What kind of business use case are you trying to solve? What are your requirements?
Hi. First, thank you for releasing this client.
I had been using the original one for the last couple of weeks and I noticed that it uses some patterns (like) that aren't so commonly found on Go clients (generated or not).
I imagine that having a leaner API using a more traditional (as in Go idiomatic) approach would be a good thing, at least for casual API consumers that are primarily Go developers – although I'm not so sure about people with plenty of experience using OpenSearch.
What is the problem? What is preventing you from meeting the requirements?
Let me try to give you a concrete example:
I imported the package to my code for the first time, and then I started looking around to see how I could do what I needed.
I quickly discovered that I needed to instantiate a client by calling
opensearch.NewClient
.Then, the next thing I discovered was the Search function there, except that I immediately got confused as to why it was defined in the confusing API struct in an oddly named
api._.go
file.opensearch-go/opensearchapi/api._.go
Lines 31 to 83 in 1aec4d8
and I started looking for usage examples just after I noticed the following
and then I discovered there are functions defined on top of this SearchRequest type
opensearch-go/opensearchapi/api.search.go
Lines 379 to 809 in 1aec4d8
which are really decorators(-ish?) on top of the much more idiomatic SearchRequest param:
opensearch-go/opensearchapi/api.search.go
Lines 59 to 117 in 1aec4d8
I see several problems with decorator functions like these.
I also noticed that gopls wasn't helping me much discover the API for the library, and I attribute part of the problem to how you're expected to build the code kind indirectly using decorators – making browsing code and documentation slower than it may be.
What are you proposing? What do you suggest we do to solve the problem or improve the existing situation?
In short, my suggestion is to do a clean-up to make the API more idiomatic to the Go ecosystem before releasing a v1 version.
What do you think?
What are your assumptions or prerequisites?
I'm assuming the current API can improve developer experience getting rid of some, apparently unimportant, odd patterns (to the Go programming language), which makes the API itself bigger than it needs to be, and harder to master.
Best regards,
Henrique.
The text was updated successfully, but these errors were encountered: