diff --git a/.env b/.env new file mode 100644 index 0000000..f8d7888 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +GITHUB_TOKEN=token diff --git a/Dockerfile.buf b/Dockerfile.buf new file mode 100644 index 0000000..2345af1 --- /dev/null +++ b/Dockerfile.buf @@ -0,0 +1,27 @@ +# Use an official Golang image +FROM golang:1.24 AS buf-builder + +# Install dependencies and clean up unnecessary package +# lists after running apt-get update and installing packages. +RUN apt-get update && apt-get install -y curl unzip && rm -rf /var/lib/apt/lists/* + +# Set Protobuf version +ENV PROTOC_VERSION=30.2 + +# Install buf +RUN curl -sSL https://github.com/bufbuild/buf/releases/latest/download/buf-Linux-x86_64 \ + -o /usr/local/bin/buf && chmod +x /usr/local/bin/buf + +# Set environment variables for Go modules +ENV GOPROXY=https://proxy.golang.org,direct +ENV GO111MODULE=on +ENV PATH="/go/bin:$PATH" + +# Install protoc-gen-go and protoc-gen-go-grpc +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31.0 && \ + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 + +WORKDIR /app + +# Copy protobuf files +COPY buf.gen.yaml buf.gen.yaml diff --git a/Dockerfile.server b/Dockerfile.server new file mode 100644 index 0000000..6bf7dcc --- /dev/null +++ b/Dockerfile.server @@ -0,0 +1,35 @@ +# Use an official Golang image for building +FROM golang:1.24 AS builder + +# Set working directory +WORKDIR /app + +# Copy Go modules and dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the entire source code +COPY . . + +# Build the Go application statically +RUN go build -o server ./server/server.go + +# Use a minimal runtime image +FROM debian:bookworm-slim +# Install CA certificates +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy the compiled binary from the builder stage +COPY --from=builder /app/server . + +# Ensure binary is executable +RUN chmod +x ./server + +# Expose required ports +EXPOSE 8080 + +# Run the server +CMD ["./server"] diff --git a/README.md b/README.md index f96c5e9..45324c4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ -# weaveGitHubSearchService -Build a service using gRPC that builds on top of the GitHub API to perform queries for the provided search phrase and allows for optional filtering down to the user level. You’ll return the file URL and the repo it was found in. +### Github Search Service +Build a service using gRPC that builds on top of the GitHub API to perform queries for the +provided search phrase and allows for optional filtering down to the user level. You’ll return the +file URL and the repo it was found in. +This is the API you’ll use to perform the GitHub search: + ```https://docs.github.com/en/rest/reference/search``` + +This is the API spec that should be implemented: + + service GithubSearchService { + rpc Search(SearchRequest) returns (SearchResponse); + } + + message SearchRequest { + string search_term = 1; + string user = 2; + } + + message SearchResponse { + repeated Result results = 1; + } + + message Result { + string file_url = 1; + string repo = 2; + } + +Instructions +1. Create a new, public GitHub repository with only a README +2. Create a new branch and do all your work in that branch +3. Create a PR back into the main branch and send in the URL to the Pull Request + +## Prerequisites +### Mac: +1. Make and Git: `brew install git make` +2. Docker: `brew install docker` +3. Ensure Docker is running: + ```sh + open -a Docker + docker info + ``` + If Docker is not running, start the Docker daemon manually. + +### Linux: +1. Make and Git: `sudo apt-get install git make` +2. Docker: `sudo apt-get install docker` +3. Ensure Docker is running: + ```sh + sudo systemctl start docker + sudo systemctl enable docker # Optional: Start Docker on boot + sudo docker info + +## Setup Instructions: +1. Clone the repository: `git clone git@github.com:Thesohan/weaveGitHubSearchService.git` +3. Generate Protobuf code (if modified): `make generate` +4. Update environement variables file `.env`: `GITHUB_API_TOKEN=` + +## Running the Service +1. To start the gRPC server and client: `make run` +2. It will ask you to `Enter username`: Enter the username for user level search result (optional) +3. It will ask you to `Enter search term`: Enter the search term to search in the GitHub repository (required) + +## Running the Tests +1. To execute a test request, run: `make test` + +## Troubleshooting +1. Authentication Errors: Ensure you have set `GITHUB_API_TOKEN` with a valid GitHub token in your env variable. +2. Ensure docker is up and running + +### Future Improvements +1. Support for configurable logging +2. Support for pagination +3. Secret management for API tokens diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..fd8e6d4 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,18 @@ +# Specify the version of the configuration file +version: v1 + +# Enable or disable managed mode +managed: + enabled: true + +# List of plugins to use for code generation +plugins: + # Go plugin for generating Go code + - name: go + out: gen/go # Output directory for generated Go code + opt: paths=source_relative # The generated .pb.go files are placed relative to the .proto file’s directory instead of following the full import path + + # Go gRPC plugin for generating gRPC code in Go + - name: go-grpc + out: gen/go + opt: paths=source_relative diff --git a/gen/go/protos/github/v1/github_search.pb.go b/gen/go/protos/github/v1/github_search.pb.go new file mode 100644 index 0000000..ee5fb08 --- /dev/null +++ b/gen/go/protos/github/v1/github_search.pb.go @@ -0,0 +1,309 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: protos/github/v1/github_search.proto + +package github_search_grpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SearchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Term string `protobuf:"bytes,1,opt,name=term,proto3" json:"term,omitempty"` + User *string `protobuf:"bytes,2,opt,name=user,proto3,oneof" json:"user,omitempty"` +} + +func (x *SearchRequest) Reset() { + *x = SearchRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_github_v1_github_search_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchRequest) ProtoMessage() {} + +func (x *SearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_github_v1_github_search_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. +func (*SearchRequest) Descriptor() ([]byte, []int) { + return file_protos_github_v1_github_search_proto_rawDescGZIP(), []int{0} +} + +func (x *SearchRequest) GetTerm() string { + if x != nil { + return x.Term + } + return "" +} + +func (x *SearchRequest) GetUser() string { + if x != nil && x.User != nil { + return *x.User + } + return "" +} + +type SearchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Results []*Result `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` +} + +func (x *SearchResponse) Reset() { + *x = SearchResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_github_v1_github_search_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResponse) ProtoMessage() {} + +func (x *SearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_github_v1_github_search_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead. +func (*SearchResponse) Descriptor() ([]byte, []int) { + return file_protos_github_v1_github_search_proto_rawDescGZIP(), []int{1} +} + +func (x *SearchResponse) GetResults() []*Result { + if x != nil { + return x.Results + } + return nil +} + +type Result struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FileUrl string `protobuf:"bytes,1,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"` + Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` +} + +func (x *Result) Reset() { + *x = Result{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_github_v1_github_search_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Result) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Result) ProtoMessage() {} + +func (x *Result) ProtoReflect() protoreflect.Message { + mi := &file_protos_github_v1_github_search_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Result.ProtoReflect.Descriptor instead. +func (*Result) Descriptor() ([]byte, []int) { + return file_protos_github_v1_github_search_proto_rawDescGZIP(), []int{2} +} + +func (x *Result) GetFileUrl() string { + if x != nil { + return x.FileUrl + } + return "" +} + +func (x *Result) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +var File_protos_github_v1_github_search_proto protoreflect.FileDescriptor + +var file_protos_github_v1_github_search_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2f, + 0x76, 0x31, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, + 0x31, 0x22, 0x45, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x17, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, + 0x07, 0x0a, 0x05, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x37, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x72, 0x65, 0x70, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x70, 0x6f, + 0x32, 0x54, 0x0a, 0x13, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x12, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xb9, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x42, 0x11, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x50, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x54, 0x68, 0x65, 0x73, 0x6f, 0x68, + 0x61, 0x6e, 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, 0x47, 0x69, 0x74, 0x48, 0x75, 0x62, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x67, 0x72, 0x70, 0x63, 0xa2, + 0x02, 0x03, 0x47, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x56, + 0x31, 0xca, 0x02, 0x09, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15, + 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x3a, 0x3a, + 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_protos_github_v1_github_search_proto_rawDescOnce sync.Once + file_protos_github_v1_github_search_proto_rawDescData = file_protos_github_v1_github_search_proto_rawDesc +) + +func file_protos_github_v1_github_search_proto_rawDescGZIP() []byte { + file_protos_github_v1_github_search_proto_rawDescOnce.Do(func() { + file_protos_github_v1_github_search_proto_rawDescData = protoimpl.X.CompressGZIP(file_protos_github_v1_github_search_proto_rawDescData) + }) + return file_protos_github_v1_github_search_proto_rawDescData +} + +var file_protos_github_v1_github_search_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_protos_github_v1_github_search_proto_goTypes = []interface{}{ + (*SearchRequest)(nil), // 0: github.v1.SearchRequest + (*SearchResponse)(nil), // 1: github.v1.SearchResponse + (*Result)(nil), // 2: github.v1.Result +} +var file_protos_github_v1_github_search_proto_depIdxs = []int32{ + 2, // 0: github.v1.SearchResponse.results:type_name -> github.v1.Result + 0, // 1: github.v1.GithubSearchService.Search:input_type -> github.v1.SearchRequest + 1, // 2: github.v1.GithubSearchService.Search:output_type -> github.v1.SearchResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_protos_github_v1_github_search_proto_init() } +func file_protos_github_v1_github_search_proto_init() { + if File_protos_github_v1_github_search_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_protos_github_v1_github_search_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protos_github_v1_github_search_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protos_github_v1_github_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_protos_github_v1_github_search_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_protos_github_v1_github_search_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_protos_github_v1_github_search_proto_goTypes, + DependencyIndexes: file_protos_github_v1_github_search_proto_depIdxs, + MessageInfos: file_protos_github_v1_github_search_proto_msgTypes, + }.Build() + File_protos_github_v1_github_search_proto = out.File + file_protos_github_v1_github_search_proto_rawDesc = nil + file_protos_github_v1_github_search_proto_goTypes = nil + file_protos_github_v1_github_search_proto_depIdxs = nil +} diff --git a/gen/go/protos/github/v1/github_search_grpc.pb.go b/gen/go/protos/github/v1/github_search_grpc.pb.go new file mode 100644 index 0000000..1188b5d --- /dev/null +++ b/gen/go/protos/github/v1/github_search_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: protos/github/v1/github_search.proto + +package github_search_grpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + GithubSearchService_Search_FullMethodName = "/github.v1.GithubSearchService/Search" +) + +// GithubSearchServiceClient is the client API for GithubSearchService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GithubSearchServiceClient interface { + Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) +} + +type githubSearchServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGithubSearchServiceClient(cc grpc.ClientConnInterface) GithubSearchServiceClient { + return &githubSearchServiceClient{cc} +} + +func (c *githubSearchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { + out := new(SearchResponse) + err := c.cc.Invoke(ctx, GithubSearchService_Search_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GithubSearchServiceServer is the server API for GithubSearchService service. +// All implementations must embed UnimplementedGithubSearchServiceServer +// for forward compatibility +type GithubSearchServiceServer interface { + Search(context.Context, *SearchRequest) (*SearchResponse, error) + mustEmbedUnimplementedGithubSearchServiceServer() +} + +// UnimplementedGithubSearchServiceServer must be embedded to have forward compatible implementations. +type UnimplementedGithubSearchServiceServer struct { +} + +func (UnimplementedGithubSearchServiceServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Search not implemented") +} +func (UnimplementedGithubSearchServiceServer) mustEmbedUnimplementedGithubSearchServiceServer() {} + +// UnsafeGithubSearchServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GithubSearchServiceServer will +// result in compilation errors. +type UnsafeGithubSearchServiceServer interface { + mustEmbedUnimplementedGithubSearchServiceServer() +} + +func RegisterGithubSearchServiceServer(s grpc.ServiceRegistrar, srv GithubSearchServiceServer) { + s.RegisterService(&GithubSearchService_ServiceDesc, srv) +} + +func _GithubSearchService_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GithubSearchServiceServer).Search(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GithubSearchService_Search_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GithubSearchServiceServer).Search(ctx, req.(*SearchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GithubSearchService_ServiceDesc is the grpc.ServiceDesc for GithubSearchService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GithubSearchService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "github.v1.GithubSearchService", + HandlerType: (*GithubSearchServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Search", + Handler: _GithubSearchService_Search_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "protos/github/v1/github_search.proto", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e74d438 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/Thesohan/weaveGitHubSearchService + +go 1.23.3 + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..092c5e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/makefile b/makefile new file mode 100644 index 0000000..b72c0c0 --- /dev/null +++ b/makefile @@ -0,0 +1,27 @@ +BUF_IMAGE=buf-builder +BUF_DOCKERFILE=Dockerfile.buf +WORKDIR=/app + +APP_IMAGE=server +APP_DOCKERFILE=Dockerfile.server + +generate: build-buf + docker run --rm -v "$(PWD):$(WORKDIR)" $(BUF_IMAGE) buf generate + +build-buf: + docker build -t $(BUF_IMAGE) -f $(BUF_DOCKERFILE) . + +build-app: + docker build -t $(APP_IMAGE) -f $(APP_DOCKERFILE) . + +run: build-app + docker run --rm -it -p 8080:8080 --env-file .env $(APP_IMAGE) + +deps: + go mod tidy + +pkg: + go mod vendor + +test: + go test ./... diff --git a/protos/github/v1/github_search.proto b/protos/github/v1/github_search.proto new file mode 100644 index 0000000..139df1c --- /dev/null +++ b/protos/github/v1/github_search.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package github.v1; + +option go_package = "github.com/Thesohan/weaveGitHubSearchService/protos/github/v1/github_search_grpc"; + +service GithubSearchService { + rpc Search(SearchRequest) returns (SearchResponse); +} + +message SearchRequest { + string term = 1; + optional string user = 2; +} + +message SearchResponse { + repeated Result results = 1; +} + +message Result { + string file_url = 1; + string repo = 2; +} diff --git a/server/github/client.go b/server/github/client.go new file mode 100644 index 0000000..2d94741 --- /dev/null +++ b/server/github/client.go @@ -0,0 +1,97 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "sync" + "time" + + httpClient "github.com/Thesohan/weaveGitHubSearchService/server/http_client" +) + +const ( + defaultGitHubAPIURL = "https://api.github.com/search/code" + githubTokenEnvName = "GITHUB_TOKEN" + httpClientTimeout = 3 * time.Second +) + +var ( + once sync.Once + singleton CodeSearcher +) + +type searchResponse struct { + Items []struct { + HTMLURL string `json:"html_url"` + Repository struct { + FullName string `json:"full_name"` + } `json:"repository"` + } `json:"items"` +} + +type githubClient struct { + client *http.Client + baseURL string + headers map[string]string + maximumRetryDelay *time.Duration +} + +// IGithubClient interface for API requests. +type CodeSearcher interface { + SearchCode(ctx context.Context, query string) (*searchResponse, error) +} + +type GitHubClientOption func(*githubClient) + +func WithBaseURL(baseURL string) GitHubClientOption { + return func(c *githubClient) { c.baseURL = baseURL } +} + +func WithToken(token string) GitHubClientOption { + return func(c *githubClient) { c.headers["Authorization"] = "token " + token } +} + +func WithMaximumRetryDelay(maximumRetryDelay *time.Duration) GitHubClientOption { + return func(c *githubClient) { c.maximumRetryDelay = maximumRetryDelay } +} + +func NewGitHubClient(opts ...GitHubClientOption) CodeSearcher { + client := &githubClient{ + client: &http.Client{Timeout: httpClientTimeout}, + baseURL: defaultGitHubAPIURL, + headers: map[string]string{ + "Content-Type": "application/json", + }, + } + + for _, opt := range opts { + opt(client) + } + if _, ok := client.headers["Authorization"]; !ok { + client.headers["Authorization"] = "token " + os.Getenv(githubTokenEnvName) + } + return client +} + +// SearchCode fetches search results using a generic request function. +func (c *githubClient) SearchCode(ctx context.Context, query string) (*searchResponse, error) { + base, err := url.Parse(c.baseURL) + if err != nil { + return nil, err + } + param := url.Values{} + param.Add("q", query) + base.RawQuery = param.Encode() + + reqURL := base.String() + // Use factory to get a GET request handler. + data := searchResponse{} + err = httpClient.DoRequest(ctx, c.client, http.MethodGet, reqURL, c.headers, nil /* body */, &data, c.maximumRetryDelay) + if err != nil { + return nil, fmt.Errorf("error from DoRequest, %v", err) + } + return &data, nil +} diff --git a/server/github/client_test.go b/server/github/client_test.go new file mode 100644 index 0000000..f5c439a --- /dev/null +++ b/server/github/client_test.go @@ -0,0 +1,73 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// Mock implementation of HTTPClient for testing. +type mockHTTPClient struct{} + +const mockData = `{ + "items": [ + { + "html_url": "https://github.com/test/repo/blob/main/file.go", + "repository": { + "full_name": "test/repo" + } + } + ] +}` + +func (m *mockHTTPClient) DoRequest(ctx context.Context, client *http.Client, url string, headers map[string]string, body interface{}, response interface{}) error { + return json.Unmarshal([]byte(mockData), response) +} + +// Test GitHubClient's SearchCode function. +func TestSearchCode(t *testing.T) { + // Start a mock HTTP server + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(mockData)) + })) + defer mockServer.Close() + + // Set up test environment + client := NewGitHubClient(WithBaseURL(mockServer.URL), WithToken("github-search-test-token")) + // Call the function + data, err := client.SearchCode(context.Background(), "test-query") + // Assertions + assert.NoError(t, err) + assert.NotNil(t, data) + assert.Len(t, data.Items, 1) + jsonData, err := json.Marshal(data) + assert.NoError(t, err) + assert.JSONEq(t, mockData, string(jsonData)) +} + +// Test SearchCode with API error response +func TestSearchCodeAPIError(t *testing.T) { + // Start a mock HTTP server that returns an error + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message": "Internal Server Error"}`)) + })) + defer mockServer.Close() + + // Set up test environment + maximumRetryDelay := 1 * time.Second + client := NewGitHubClient(WithBaseURL(mockServer.URL), WithToken("github-search-token"), WithMaximumRetryDelay(&maximumRetryDelay)) + // Call the function + data, err := client.SearchCode(context.Background(), "test-query") + + // Assertions + assert.Error(t, err) + assert.Nil(t, data) +} diff --git a/server/http_client/client.go b/server/http_client/client.go new file mode 100644 index 0000000..56d7dbb --- /dev/null +++ b/server/http_client/client.go @@ -0,0 +1,87 @@ +package httpclient + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/cenkalti/backoff/v4" +) + +const defaultMaximumRetryDelay = 30 * time.Second + +// DoRequest performs an HTTP request with retries, request body handling, and response decoding. +func DoRequest(ctx context.Context, client *http.Client, method, url string, headers map[string]string, body interface{}, response interface{}, maximumRetryDelay *time.Duration) error { + var requestBody io.Reader + + // Encode request body if provided + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("failed to marshal request body: %w", err) + } + requestBody = bytes.NewReader(data) + } + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, method, url, requestBody) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + for key, value := range headers { + req.Header.Set(key, value) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + return executeRequest(client, req, response, maximumRetryDelay) +} + +// executeRequest performs the HTTP request with retries and handles response parsing. +func executeRequest(client *http.Client, req *http.Request, response interface{}, maximumRetryDelay *time.Duration) error { + bo := backoff.NewExponentialBackOff() + bo.MaxElapsedTime = defaultMaximumRetryDelay + if maximumRetryDelay != nil { + bo.MaxElapsedTime = *maximumRetryDelay + } + + operation := func() error { + log.Printf("HTTP %s request to %s at: %v", req.Method, req.URL, time.Now().Format(time.RFC3339)) + + resp, err := client.Do(req) + if err != nil { + log.Printf("Request failed: %v", err) + return err + } + defer resp.Body.Close() + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("API error: %s", resp.Status) + } + + if response != nil { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + if err := json.Unmarshal(bodyBytes, response); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + return nil + } + return nil + } + + if err := backoff.Retry(operation, bo); err != nil { + return fmt.Errorf("request failed after retries: %w", err) + } + return nil +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..d01a689 --- /dev/null +++ b/server/server.go @@ -0,0 +1,138 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "net" + "os" + "strings" + "text/tabwriter" + "time" + + pb "github.com/Thesohan/weaveGitHubSearchService/gen/go/protos/github/v1" + "github.com/Thesohan/weaveGitHubSearchService/server/github" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const ( + serverAddress string = ":8000" + tcpNetwork = "tcp" +) + +// gitHubSearchService implements the gRPC service. +type gitHubSearchService struct { + pb.UnimplementedGithubSearchServiceServer + ghClient github.CodeSearcher +} + +// Search handles search requests. +func (s *gitHubSearchService) Search(ctx context.Context, searchReq *pb.SearchRequest) (*pb.SearchResponse, error) { + // Validate the request payload + if searchReq.Term == "" { + return nil, fmt.Errorf("search term is required") + } + + query := buildGitHubQuery(searchReq) + data, err := s.ghClient.SearchCode(ctx, query) + if err != nil { + return nil, fmt.Errorf("error while searching code %v", err) + } + var results []*pb.Result + for _, item := range data.Items { + results = append(results, &pb.Result{ + FileUrl: item.HTMLURL, + Repo: item.Repository.FullName, + }) + } + return &pb.SearchResponse{Results: results}, nil +} + +// buildGitHubQuery constructs the search query. +func buildGitHubQuery(searchReq *pb.SearchRequest) string { + query := searchReq.Term + if searchReq.GetUser() != "" { + query += " user:" + searchReq.GetUser() + } + return query +} +func runServer() { + listener, err := net.Listen(tcpNetwork, serverAddress) + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + + grpcServer := grpc.NewServer() + githubClient := github.NewGitHubClient() + pb.RegisterGithubSearchServiceServer(grpcServer, &gitHubSearchService{ghClient: githubClient}) + + log.Printf("gRPC server running on port %v...", serverAddress) + if err := grpcServer.Serve(listener); err != nil { + log.Fatalf("Failed to serve: %v", err) + } + +} +func runClient() { + // Establish a connection to the gRPC server with insecure credentials + conn, err := grpc.NewClient("localhost:8000", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("fail to dial: %v", err) + } + defer conn.Close() + + // Create a new client for the GithubSearchService + client := pb.NewGithubSearchServiceClient(conn) + for { + // Get user input for username and search term + log.Print("\nEnter username: ") + var userInput string + fmt.Scanln(&userInput) // fmt.Scanln` stops after reading the first whitespace, user can't have any whitspace in github + var user *string + if userInput != "" { + user = &userInput + } + log.Print("Enter search term: ") + reader := bufio.NewReader(os.Stdin) + searchTerm, err := reader.ReadString('\n') // `bufio.NewReader` stops after the given delimiter `\n`. searchTerm can be a complete sentence. + if err != nil { + log.Fatal(err) + } + log.Printf("Searching for %v", searchTerm) + // Remove newline character from search term + searchTerm = strings.TrimSuffix(searchTerm, "\n") + log.Println(searchTerm) + req := &pb.SearchRequest{ + Term: searchTerm, + User: user, // Set to a username if needed + } + + // Perform the search request + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + resp, err := client.Search(ctx, req) + if err != nil { + log.Printf("Search failed: %v", err) + continue + } + + // Create a new tabwriter + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.TabIndent) + // Print header + fmt.Fprintln(w, "S.N\tFile\tRepo") + for index, result := range resp.Results { + fmt.Fprintf(w, "%d\t%s\t%s\n", index, result.FileUrl, result.Repo) + } + // Flush the writer to ensure output is displayed + if err := w.Flush(); err != nil { + log.Printf("Failed to flush writer: %v", err) + } + } + +} +func main() { + go runServer() + time.Sleep(5 * time.Second) // Waiting for 5 second for server to start + runClient() +} diff --git a/v1/github_search.pb.go b/v1/github_search.pb.go new file mode 100644 index 0000000..ee5fb08 --- /dev/null +++ b/v1/github_search.pb.go @@ -0,0 +1,309 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: protos/github/v1/github_search.proto + +package github_search_grpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SearchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Term string `protobuf:"bytes,1,opt,name=term,proto3" json:"term,omitempty"` + User *string `protobuf:"bytes,2,opt,name=user,proto3,oneof" json:"user,omitempty"` +} + +func (x *SearchRequest) Reset() { + *x = SearchRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_github_v1_github_search_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchRequest) ProtoMessage() {} + +func (x *SearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_protos_github_v1_github_search_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. +func (*SearchRequest) Descriptor() ([]byte, []int) { + return file_protos_github_v1_github_search_proto_rawDescGZIP(), []int{0} +} + +func (x *SearchRequest) GetTerm() string { + if x != nil { + return x.Term + } + return "" +} + +func (x *SearchRequest) GetUser() string { + if x != nil && x.User != nil { + return *x.User + } + return "" +} + +type SearchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Results []*Result `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` +} + +func (x *SearchResponse) Reset() { + *x = SearchResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_github_v1_github_search_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResponse) ProtoMessage() {} + +func (x *SearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_protos_github_v1_github_search_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead. +func (*SearchResponse) Descriptor() ([]byte, []int) { + return file_protos_github_v1_github_search_proto_rawDescGZIP(), []int{1} +} + +func (x *SearchResponse) GetResults() []*Result { + if x != nil { + return x.Results + } + return nil +} + +type Result struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FileUrl string `protobuf:"bytes,1,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"` + Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` +} + +func (x *Result) Reset() { + *x = Result{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_github_v1_github_search_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Result) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Result) ProtoMessage() {} + +func (x *Result) ProtoReflect() protoreflect.Message { + mi := &file_protos_github_v1_github_search_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Result.ProtoReflect.Descriptor instead. +func (*Result) Descriptor() ([]byte, []int) { + return file_protos_github_v1_github_search_proto_rawDescGZIP(), []int{2} +} + +func (x *Result) GetFileUrl() string { + if x != nil { + return x.FileUrl + } + return "" +} + +func (x *Result) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +var File_protos_github_v1_github_search_proto protoreflect.FileDescriptor + +var file_protos_github_v1_github_search_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2f, + 0x76, 0x31, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, + 0x31, 0x22, 0x45, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x17, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, + 0x07, 0x0a, 0x05, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x37, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x72, 0x65, 0x70, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x70, 0x6f, + 0x32, 0x54, 0x0a, 0x13, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x12, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xb9, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x76, 0x31, 0x42, 0x11, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x50, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x54, 0x68, 0x65, 0x73, 0x6f, 0x68, + 0x61, 0x6e, 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, 0x47, 0x69, 0x74, 0x48, 0x75, 0x62, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x67, 0x72, 0x70, 0x63, 0xa2, + 0x02, 0x03, 0x47, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x56, + 0x31, 0xca, 0x02, 0x09, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15, + 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x3a, 0x3a, + 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_protos_github_v1_github_search_proto_rawDescOnce sync.Once + file_protos_github_v1_github_search_proto_rawDescData = file_protos_github_v1_github_search_proto_rawDesc +) + +func file_protos_github_v1_github_search_proto_rawDescGZIP() []byte { + file_protos_github_v1_github_search_proto_rawDescOnce.Do(func() { + file_protos_github_v1_github_search_proto_rawDescData = protoimpl.X.CompressGZIP(file_protos_github_v1_github_search_proto_rawDescData) + }) + return file_protos_github_v1_github_search_proto_rawDescData +} + +var file_protos_github_v1_github_search_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_protos_github_v1_github_search_proto_goTypes = []interface{}{ + (*SearchRequest)(nil), // 0: github.v1.SearchRequest + (*SearchResponse)(nil), // 1: github.v1.SearchResponse + (*Result)(nil), // 2: github.v1.Result +} +var file_protos_github_v1_github_search_proto_depIdxs = []int32{ + 2, // 0: github.v1.SearchResponse.results:type_name -> github.v1.Result + 0, // 1: github.v1.GithubSearchService.Search:input_type -> github.v1.SearchRequest + 1, // 2: github.v1.GithubSearchService.Search:output_type -> github.v1.SearchResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_protos_github_v1_github_search_proto_init() } +func file_protos_github_v1_github_search_proto_init() { + if File_protos_github_v1_github_search_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_protos_github_v1_github_search_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protos_github_v1_github_search_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protos_github_v1_github_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_protos_github_v1_github_search_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_protos_github_v1_github_search_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_protos_github_v1_github_search_proto_goTypes, + DependencyIndexes: file_protos_github_v1_github_search_proto_depIdxs, + MessageInfos: file_protos_github_v1_github_search_proto_msgTypes, + }.Build() + File_protos_github_v1_github_search_proto = out.File + file_protos_github_v1_github_search_proto_rawDesc = nil + file_protos_github_v1_github_search_proto_goTypes = nil + file_protos_github_v1_github_search_proto_depIdxs = nil +} diff --git a/v1/github_search_grpc.pb.go b/v1/github_search_grpc.pb.go new file mode 100644 index 0000000..1188b5d --- /dev/null +++ b/v1/github_search_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: protos/github/v1/github_search.proto + +package github_search_grpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + GithubSearchService_Search_FullMethodName = "/github.v1.GithubSearchService/Search" +) + +// GithubSearchServiceClient is the client API for GithubSearchService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GithubSearchServiceClient interface { + Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) +} + +type githubSearchServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGithubSearchServiceClient(cc grpc.ClientConnInterface) GithubSearchServiceClient { + return &githubSearchServiceClient{cc} +} + +func (c *githubSearchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { + out := new(SearchResponse) + err := c.cc.Invoke(ctx, GithubSearchService_Search_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GithubSearchServiceServer is the server API for GithubSearchService service. +// All implementations must embed UnimplementedGithubSearchServiceServer +// for forward compatibility +type GithubSearchServiceServer interface { + Search(context.Context, *SearchRequest) (*SearchResponse, error) + mustEmbedUnimplementedGithubSearchServiceServer() +} + +// UnimplementedGithubSearchServiceServer must be embedded to have forward compatible implementations. +type UnimplementedGithubSearchServiceServer struct { +} + +func (UnimplementedGithubSearchServiceServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Search not implemented") +} +func (UnimplementedGithubSearchServiceServer) mustEmbedUnimplementedGithubSearchServiceServer() {} + +// UnsafeGithubSearchServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GithubSearchServiceServer will +// result in compilation errors. +type UnsafeGithubSearchServiceServer interface { + mustEmbedUnimplementedGithubSearchServiceServer() +} + +func RegisterGithubSearchServiceServer(s grpc.ServiceRegistrar, srv GithubSearchServiceServer) { + s.RegisterService(&GithubSearchService_ServiceDesc, srv) +} + +func _GithubSearchService_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GithubSearchServiceServer).Search(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GithubSearchService_Search_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GithubSearchServiceServer).Search(ctx, req.(*SearchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GithubSearchService_ServiceDesc is the grpc.ServiceDesc for GithubSearchService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GithubSearchService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "github.v1.GithubSearchService", + HandlerType: (*GithubSearchServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Search", + Handler: _GithubSearchService_Search_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "protos/github/v1/github_search.proto", +}