github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/charmhub/find.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charmhub
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  
    13  	"github.com/juju/juju/charmhub/path"
    14  	"github.com/juju/juju/charmhub/transport"
    15  )
    16  
    17  // FindOption to be passed to Find to customize the resulting request.
    18  type FindOption func(*findOptions)
    19  
    20  type findOptions struct {
    21  	category         *string
    22  	channel          *string
    23  	charmType        *string
    24  	platforms        *string
    25  	publisher        *string
    26  	relationRequires *string
    27  	relationProvides *string
    28  }
    29  
    30  // WithFindCategory sets the category on the option.
    31  func WithFindCategory(category string) FindOption {
    32  	return func(findOptions *findOptions) {
    33  		findOptions.category = &category
    34  	}
    35  }
    36  
    37  // WithFindChannel sets the channel on the option.
    38  func WithFindChannel(channel string) FindOption {
    39  	return func(findOptions *findOptions) {
    40  		findOptions.channel = &channel
    41  	}
    42  }
    43  
    44  // WithFindType sets the charmType on the option.
    45  func WithFindType(charmType string) FindOption {
    46  	return func(findOptions *findOptions) {
    47  		findOptions.charmType = &charmType
    48  	}
    49  }
    50  
    51  // WithFindPlatforms sets the charmPlatforms on the option.
    52  func WithFindPlatforms(platforms string) FindOption {
    53  	return func(findOptions *findOptions) {
    54  		findOptions.platforms = &platforms
    55  	}
    56  }
    57  
    58  // WithFindPublisher sets the publisher on the option.
    59  func WithFindPublisher(publisher string) FindOption {
    60  	return func(findOptions *findOptions) {
    61  		findOptions.publisher = &publisher
    62  	}
    63  }
    64  
    65  // WithFindRelationRequires sets the relationRequires on the option.
    66  func WithFindRelationRequires(relationRequires string) FindOption {
    67  	return func(findOptions *findOptions) {
    68  		findOptions.relationRequires = &relationRequires
    69  	}
    70  }
    71  
    72  // WithFindRelationProvides sets the relationProvides on the option.
    73  func WithFindRelationProvides(relationProvides string) FindOption {
    74  	return func(findOptions *findOptions) {
    75  		findOptions.relationProvides = &relationProvides
    76  	}
    77  }
    78  
    79  // Create a findOptions instance with default values.
    80  func newFindOptions() *findOptions {
    81  	return &findOptions{}
    82  }
    83  
    84  // findClient defines a client for querying information about a given charm or
    85  // bundle for a given CharmHub store.
    86  type findClient struct {
    87  	path   path.Path
    88  	client RESTClient
    89  	logger Logger
    90  }
    91  
    92  // newFindClient creates a findClient for querying charm or bundle information.
    93  func newFindClient(path path.Path, client RESTClient, logger Logger) *findClient {
    94  	return &findClient{
    95  		path:   path,
    96  		client: client,
    97  		logger: logger,
    98  	}
    99  }
   100  
   101  // Find searches Charm Hub and provides results matching a string.
   102  func (c *findClient) Find(ctx context.Context, query string, options ...FindOption) ([]transport.FindResponse, error) {
   103  	opts := newFindOptions()
   104  	for _, option := range options {
   105  		option(opts)
   106  	}
   107  
   108  	c.logger.Tracef("Find(%s)", query)
   109  	path, err := c.path.Query("q", query)
   110  	if err != nil {
   111  		return nil, errors.Trace(err)
   112  	}
   113  
   114  	path, err = path.Query("fields", defaultFindFilter())
   115  	if err != nil {
   116  		return nil, errors.Trace(err)
   117  	}
   118  
   119  	if err := walkFindOptions(opts, func(name, value string) error {
   120  		path, err = path.Query(name, value)
   121  		return errors.Trace(err)
   122  	}); err != nil {
   123  		return nil, errors.Trace(err)
   124  	}
   125  
   126  	var resp transport.FindResponses
   127  	restResp, err := c.client.Get(ctx, path, &resp)
   128  	if err != nil {
   129  		return nil, errors.Trace(err)
   130  	}
   131  	if restResp.StatusCode == http.StatusNotFound {
   132  		return nil, errors.NotFoundf(query)
   133  	}
   134  	if err := handleBasicAPIErrors(resp.ErrorList, c.logger); err != nil {
   135  		return nil, errors.Trace(err)
   136  	}
   137  
   138  	return resp.Results, nil
   139  }
   140  
   141  func walkFindOptions(opts *findOptions, fn func(string, string) error) error {
   142  	// We could use reflect here, but it might be easier to just list out what
   143  	// we want to walk over.
   144  	// See: https://gist.github.com/SimonRichardson/7c9243d71551cad4af7661128add93b5
   145  	if opts.category != nil {
   146  		if err := fn("category", *opts.category); err != nil {
   147  			return errors.Trace(err)
   148  		}
   149  	}
   150  	if opts.channel != nil {
   151  		if err := fn("channel", *opts.channel); err != nil {
   152  			return errors.Trace(err)
   153  		}
   154  	}
   155  	if opts.charmType != nil {
   156  		if err := fn("type", *opts.charmType); err != nil {
   157  			return errors.Trace(err)
   158  		}
   159  	}
   160  	if opts.platforms != nil {
   161  		if err := fn("platforms", *opts.platforms); err != nil {
   162  			return errors.Trace(err)
   163  		}
   164  	}
   165  	if opts.publisher != nil {
   166  		if err := fn("publisher", *opts.publisher); err != nil {
   167  			return errors.Trace(err)
   168  		}
   169  	}
   170  	if opts.relationRequires != nil {
   171  		if err := fn("relation-requires", *opts.relationRequires); err != nil {
   172  			return errors.Trace(err)
   173  		}
   174  	}
   175  	if opts.relationProvides != nil {
   176  		if err := fn("relation-provides", *opts.relationProvides); err != nil {
   177  			return errors.Trace(err)
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  // defaultFindFilter returns a filter string to retrieve all data
   184  // necessary to fill the transport.FindResponse.  Without it, we'd
   185  // receive the Name, ID and Type.
   186  func defaultFindFilter() string {
   187  	filter := defaultFindResultFilter
   188  	filter = append(filter, appendFilterList("default-release", defaultRevisionFilter)...)
   189  	return strings.Join(filter, ",")
   190  }
   191  
   192  var defaultFindResultFilter = []string{
   193  	"result.publisher.display-name",
   194  	"result.summary",
   195  	"result.store-url",
   196  }
   197  
   198  var defaultRevisionFilter = []string{
   199  	"revision.bases.architecture",
   200  	"revision.bases.name",
   201  	"revision.bases.channel",
   202  	"revision.version",
   203  }