From bc9a954f523c583843b44c8109dad8c478ea234e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Go=C3=B1i?= Date: Thu, 5 Mar 2026 19:53:03 -0300 Subject: [PATCH 01/13] feat(components): Adds component and version status service --- .env.example | 6 + go.mod | 69 ++++--- go.sum | 152 +++++++------- pkg/config/server_config.go | 3 + pkg/dtos/component_status_input.go | 63 ++++++ pkg/dtos/component_status_output.go | 96 +++++++++ pkg/models/component_status.go | 211 +++++++++++++++++++ pkg/models/component_status_test.go | 226 +++++++++++++++++++++ pkg/models/tests/all_urls.sql | 10 +- pkg/models/tests/projects.sql | 8 +- pkg/models/tests/versions.sql | 4 + pkg/service/component_service.go | 96 ++++++++- pkg/service/component_service_test.go | 77 +++++++ pkg/service/component_support.go | 84 ++++++++ pkg/usecase/component.go | 173 +++++++++++++++- pkg/usecase/component_test.go | 280 +++++++++++++++++++++++++- pkg/usecase/status_mapper.go | 87 ++++++++ pkg/usecase/status_mapper_test.go | 203 +++++++++++++++++++ 18 files changed, 1731 insertions(+), 117 deletions(-) create mode 100644 pkg/dtos/component_status_input.go create mode 100644 pkg/dtos/component_status_output.go create mode 100644 pkg/models/component_status.go create mode 100644 pkg/models/component_status_test.go create mode 100644 pkg/usecase/status_mapper.go create mode 100644 pkg/usecase/status_mapper_test.go diff --git a/.env.example b/.env.example index 36797ac..3a25298 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ APP_DEBUG="true" DB_DRIVER="sqlite" DB_DSN="sqlite/testdb.db?cache=shared&mode=memory" + +# Status Mapping Configuration (JSON format) +# Maps database statuses to classified statuses for API responses +# Default mappings: unlisted->removed, yanked->removed, deleted->deleted, +# deprecated->deprecated, unpublished->removed, archived->deprecated, active->active +# STATUS_MAPPING='{"unlisted":"removed","yanked":"removed","deleted":"deleted","deprecated":"deprecated","unpublished":"removed","archived":"deprecated","active":"active"}' diff --git a/go.mod b/go.mod index db385fb..829dd0a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module scanoss.com/components -go 1.24 +go 1.24.0 toolchain go1.24.6 @@ -9,24 +9,27 @@ require ( github.com/google/go-cmp v0.7.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jmoiron/sqlx v1.4.0 - github.com/lib/pq v1.10.9 - github.com/scanoss/go-grpc-helper v0.11.0 - github.com/scanoss/go-models v0.4.0 + github.com/lib/pq v1.11.2 + github.com/scanoss/go-component-helper v0.1.0 + github.com/scanoss/go-grpc-helper v0.13.0 + github.com/scanoss/go-models v0.5.1 github.com/scanoss/go-purl-helper v0.2.1 - github.com/scanoss/papi v0.28.0 + github.com/scanoss/papi v0.31.0 github.com/scanoss/zap-logging-helper v0.4.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/metric v1.38.0 - go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.75.0 - modernc.org/sqlite v1.38.2 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/metric v1.40.0 + go.uber.org/zap v1.27.1 + google.golang.org/grpc v1.79.1 + modernc.org/sqlite v1.46.1 ) // replace github.com/scanoss/papi => ../papi require ( github.com/BurntSushi/toml v1.2.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -35,39 +38,43 @@ require ( github.com/golobby/dotenv v1.3.2 // indirect github.com/golobby/env/v2 v2.2.4 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/package-url/packageurl-go v0.1.3 // indirect github.com/phuslu/iploc v1.0.20230201 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/scanoss/ipfilter/v2 v2.0.2 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.37.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.66.3 // indirect + modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) // Details of how to use the "replace" command for local development // https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive -// ie. replace github.com/scanoss/papi => ../papi +// ie. +replace github.com/scanoss/papi => ../papi + +//replace github.com/scanoss/go-component-helper => ../go-component-helper + // require github.com/scanoss/papi v0.0.0-unpublished //replace github.com/scanoss/go-grpc-helper v0.6.0 => ../go-grpc-helper diff --git a/go.sum b/go.sum index b4c5901..c7a1331 100644 --- a/go.sum +++ b/go.sum @@ -391,17 +391,21 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -562,10 +566,13 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vb github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -585,16 +592,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= @@ -614,18 +622,18 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/scanoss/go-grpc-helper v0.11.0 h1:DifUX7KrQObTo9ta/vc4vqSzAdDEy1yNl+zWKuX5iOc= -github.com/scanoss/go-grpc-helper v0.11.0/go.mod h1:p2lhQTs6X5Y4E2F50qG6DbGpATtX/YYMycEcFwo9XVE= -github.com/scanoss/go-models v0.4.0 h1:TPAWgFzseChYe12RHVcsfdouZH8AleiPphKA7TwOd04= -github.com/scanoss/go-models v0.4.0/go.mod h1:Dq8ag9CI/3h0sqDWYUrTjW/jO8l5L6oopWJRKtJxzqA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/scanoss/go-component-helper v0.1.0 h1:lybXLkWnpTTZ/XQecUXRmZXCaYnpUQ/qU3xHOwNWOAs= +github.com/scanoss/go-component-helper v0.1.0/go.mod h1:JrkReO2ODXkc0N6CmGGdosQRgfxRZCCwPHxSWkVaruY= +github.com/scanoss/go-grpc-helper v0.13.0 h1:GtKDKuc2jTtv27naIVF3f095tuJvSvxbeN+HmUdBS4Y= +github.com/scanoss/go-grpc-helper v0.13.0/go.mod h1:mNfM/788jzo8rQ2K2Y97UYQPd6MjuVXEM7r05kWPOJc= +github.com/scanoss/go-models v0.5.1 h1:MdHRhQcjG6sz5BLbyWfu/+jfH7I4Snnpo9XriukB7GI= +github.com/scanoss/go-models v0.5.1/go.mod h1:7r2ylOddgwMx8uAf7dvYBiThiU5eeVwHVvVgsVHtn3Q= github.com/scanoss/go-purl-helper v0.2.1 h1:jp960a585ycyJSlqZky1NatMJBIQi/JGITDfNSu/9As= github.com/scanoss/go-purl-helper v0.2.1/go.mod h1:v20/bKD8G+vGrILdiq6r0hyRD2bO8frCJlu9drEcQ38= github.com/scanoss/ipfilter/v2 v2.0.2 h1:GaB9i8kVJg9JQZm5XGStYkEpiaCVdsrj7ezI2wV/oh8= github.com/scanoss/ipfilter/v2 v2.0.2/go.mod h1:AwrpX4XGbZ7EKISMi1d6E5csBk1nWB8+ugpvXHFcTpA= -github.com/scanoss/papi v0.28.0 h1:uvevFYoxwzvSH1hvgBoAkScIGTK2U1+rLzHSoJdnARk= -github.com/scanoss/papi v0.28.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= github.com/scanoss/zap-logging-helper v0.4.0 h1:2qTYoaFa9+MlD2/1wmPtiDHfh+42NIEwgKVU3rPpl0Y= github.com/scanoss/zap-logging-helper v0.4.0/go.mod h1:9QuEZcq73g/0Izv1tWeOWukoIK0oTBzM4jSNQ5kRR1w= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -664,30 +672,30 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -696,8 +704,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -717,8 +725,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -747,8 +755,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -800,8 +808,8 @@ golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -843,8 +851,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -915,8 +923,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -933,8 +941,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -997,8 +1005,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1180,10 +1188,10 @@ google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZV google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1222,8 +1230,8 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1240,8 +1248,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -1261,18 +1269,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= -modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= -modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= -modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1281,8 +1291,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 58c10ea..1c150d4 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -68,6 +68,9 @@ type ServerConfig struct { BlockByDefault bool `env:"COMP_BLOCK_BY_DEFAULT"` // Block request by default if they are not in the allow list TrustProxy bool `env:"COMP_TRUST_PROXY"` // Trust the interim proxy or not (causes the source IP to be validated instead of the proxy) } + StatusMapping struct { + Mapping string `env:"STATUS_MAPPING"` // JSON string mapping DB statuses to classified statuses, e.g. {"unlisted":"removed","yanked":"removed"} + } } // NewServerConfig loads all config options and return a struct for use diff --git a/pkg/dtos/component_status_input.go b/pkg/dtos/component_status_input.go new file mode 100644 index 0000000..acdcb48 --- /dev/null +++ b/pkg/dtos/component_status_input.go @@ -0,0 +1,63 @@ +package dtos + +import ( + "encoding/json" + "errors" + "fmt" + "go.uber.org/zap" +) + +// ComponentStatusInput represents a single component status request +type ComponentStatusInput struct { + Purl string `json:"purl"` + Requirement string `json:"requirement,omitempty"` +} + +// ComponentsStatusInput represents a request for multiple component statuses +type ComponentsStatusInput struct { + Components []ComponentStatusInput `json:"components"` +} + +func ExportComponentStatusInput(s *zap.SugaredLogger, output ComponentStatusInput) ([]byte, error) { + data, err := json.Marshal(output) + if err != nil { + s.Errorf("Parse failure: %v", err) + return nil, errors.New("failed to produce JSON ") + } + return data, nil +} + +func ParseComponentStatusInput(s *zap.SugaredLogger, input []byte) (ComponentStatusInput, error) { + if len(input) == 0 { + return ComponentStatusInput{}, errors.New("no data supplied to parse") + } + var data ComponentStatusInput + err := json.Unmarshal(input, &data) + if err != nil { + s.Errorf("Parse failure: %v", err) + return ComponentStatusInput{}, fmt.Errorf("failed to parse data: %v", err) + } + return data, nil +} + +func ExportComponentsStatusInput(s *zap.SugaredLogger, output ComponentsStatusInput) ([]byte, error) { + data, err := json.Marshal(output) + if err != nil { + s.Errorf("Parse failure: %v", err) + return nil, errors.New("failed to produce JSON ") + } + return data, nil +} + +func ParseComponentsStatusInput(s *zap.SugaredLogger, input []byte) (ComponentsStatusInput, error) { + if len(input) == 0 { + return ComponentsStatusInput{}, errors.New("no data supplied to parse") + } + var data ComponentsStatusInput + err := json.Unmarshal(input, &data) + if err != nil { + s.Errorf("Parse failure: %v", err) + return ComponentsStatusInput{}, fmt.Errorf("failed to parse data: %v", err) + } + return data, nil +} diff --git a/pkg/dtos/component_status_output.go b/pkg/dtos/component_status_output.go new file mode 100644 index 0000000..9fd0588 --- /dev/null +++ b/pkg/dtos/component_status_output.go @@ -0,0 +1,96 @@ +package dtos + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" + "go.uber.org/zap" +) + +// ComponentStatusOutput represents the status information for a single component +type ComponentStatusOutput struct { + Purl string `json:"purl"` + Name string `json:"name"` + Requirement string `json:"requirement,omitempty"` + VersionStatus *VersionStatusOutput `json:"version_status,omitempty"` + ComponentStatus *ComponentStatusInfo `json:"component_status,omitempty"` +} + +// VersionStatusOutput represents the status of a specific version +type VersionStatusOutput struct { + Version string `json:"version"` + Status string `json:"status"` + RepositoryStatus string `json:"repository_status,omitempty"` + IndexedDate string `json:"indexed_date,omitempty"` + StatusChangeDate string `json:"status_change_date,omitempty"` + ErrorMessage *string `json:"error_message,omitempty"` + ErrorCode *domain.StatusCode `json:"error_code,omitempty"` +} + +// ComponentStatusInfo represents the status of a component (ignoring version) +type ComponentStatusInfo struct { + Status string `json:"status"` + RepositoryStatus string `json:"repository_status,omitempty"` + FirstIndexedDate string `json:"first_indexed_date,omitempty"` + LastIndexedDate string `json:"last_indexed_date,omitempty"` + StatusChangeDate string `json:"status_change_date,omitempty"` + ErrorMessage *string `json:"error_message,omitempty"` + ErrorCode *domain.StatusCode `json:"error_code,omitempty"` +} + +// ComponentsStatusOutput represents the status information for multiple components +type ComponentsStatusOutput struct { + Components []ComponentStatusOutput `json:"components"` +} + +func ExportComponentStatusOutput(s *zap.SugaredLogger, output ComponentStatusOutput) ([]byte, error) { + data, err := json.Marshal(output) + if err != nil { + s.Errorf("Parse failure: %v", err) + return nil, errors.New("failed to produce JSON ") + } + return data, nil +} + +func ParseComponentStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentStatusOutput, error) { + if len(output) == 0 { + return ComponentStatusOutput{}, errors.New("no data supplied to parse") + } + var data ComponentStatusOutput + err := json.Unmarshal(output, &data) + if err != nil { + s.Errorf("Parse failure: %v", err) + return ComponentStatusOutput{}, fmt.Errorf("failed to parse data: %v", err) + } + return data, nil +} + +func ExportComponentsStatusOutput(s *zap.SugaredLogger, output ComponentsStatusOutput) ([]byte, error) { + data, err := json.Marshal(output) + if err != nil { + s.Errorf("Parse failure: %v", err) + return nil, errors.New("failed to produce JSON ") + } + return data, nil +} + +func ParseComponentsStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentsStatusOutput, error) { + if len(output) == 0 { + return ComponentsStatusOutput{}, errors.New("no data supplied to parse") + } + var data ComponentsStatusOutput + err := json.Unmarshal(output, &data) + if err != nil { + s.Errorf("Parse failure: %v", err) + return ComponentsStatusOutput{}, fmt.Errorf("failed to parse data: %v", err) + } + return data, nil +} + +// StringPtr returns a pointer to the provided string value +// This is useful for optional string fields that require pointers +func StringPtr(s string) *string { + return &s +} diff --git a/pkg/models/component_status.go b/pkg/models/component_status.go new file mode 100644 index 0000000..65af0b2 --- /dev/null +++ b/pkg/models/component_status.go @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package models + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + purlhelper "github.com/scanoss/go-purl-helper/pkg" + "go.uber.org/zap" +) + +type ComponentStatusModel struct { + ctx context.Context + s *zap.SugaredLogger + q *database.DBQueryContext +} + +// ComponentVersionStatus represents the status information for a specific version +type ComponentVersionStatus struct { + PurlName string `db:"purl_name"` + Version string `db:"version"` + IndexedDate sql.NullString `db:"indexed_date"` + VersionStatus sql.NullString `db:"version_status"` + VersionStatusChangeDate sql.NullString `db:"version_status_change_date"` +} + +// ComponentProjectStatus represents the status information for a component (ignoring version) +type ComponentProjectStatus struct { + PurlName string `db:"purl_name"` + Component string `db:"component"` + FirstIndexedDate sql.NullString `db:"first_indexed_date"` + LastIndexedDate sql.NullString `db:"last_indexed_date"` + Status sql.NullString `db:"status"` + StatusChangeDate sql.NullString `db:"status_change_date"` +} + +// ComponentFullStatus combines version and project status information +type ComponentFullStatus struct { + ComponentVersionStatus + ComponentProjectStatus +} + +func NewComponentStatusModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext) *ComponentStatusModel { + return &ComponentStatusModel{ctx: ctx, s: s, q: q} +} + +// GetComponentStatusByPurlAndVersion gets status information for a specific component version +func (m *ComponentStatusModel) GetComponentStatusByPurlAndVersion(purlString, version string) (*ComponentVersionStatus, error) { + if len(purlString) == 0 { + m.s.Errorf("Please specify a valid Purl String to query") + return nil, errors.New("please specify a valid Purl String to query") + } + + purl, err := purlhelper.PurlFromString(purlString) + if err != nil { + return nil, err + } + + purlName, err := purlhelper.PurlNameFromString(purlString) + if err != nil { + return nil, err + } + + var status ComponentVersionStatus + + // Query to get both version and component status + query := ` + select distinct au.purl_name, au."version", au.indexed_date, au.version_status, au.version_status_change_date + from + all_urls au, + mines m + where + au.mine_id = m.id and au.purl_name = $1 and m.purl_type = $2 and au."version" = $3 + ` + + var results []ComponentVersionStatus + err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type, version) + if err != nil { + m.s.Errorf("Failed to query component status for %v version %v: %v", purlName, version, err) + return nil, fmt.Errorf("failed to query component status: %v", err) + } + if len(results) == 0 { + m.s.Warnf("No status found for %v version %v", purlName, version) + return nil, fmt.Errorf("component version not found") + } + status = results[0] + + m.s.Debugf("Found status for %v version %v", purlName, version) + return &status, nil +} + +// GetComponentStatusByPurl gets status information for the latest version of a component +func (m *ComponentStatusModel) GetComponentStatusByPurl(purlString string) (*ComponentProjectStatus, error) { + if len(purlString) == 0 { + m.s.Errorf("Please specify a valid Purl String to query") + return nil, errors.New("please specify a valid Purl String to query") + } + + purl, err := purlhelper.PurlFromString(purlString) + if err != nil { + return nil, err + } + + purlName, err := purlhelper.PurlNameFromString(purlString) + if err != nil { + return nil, err + } + + var status ComponentProjectStatus + + // Query to get both version and component status for the latest version + query := ` + select distinct + p.component, + p.first_indexed_date, + p.last_indexed_date , + p.status, + p.status_change_date + from + projects p, + mines m + where + p.mine_id = m.id + and p.purl_name = $1 + and m.purl_type = $2; + ` + + var results []ComponentProjectStatus + err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type) + if err != nil { + m.s.Errorf("Failed to query component status for %v: %v", purlName, err) + return nil, fmt.Errorf("failed to query component status: %v", err) + } + if len(results) == 0 { + m.s.Warnf("No status found for %v", purlName) + return nil, fmt.Errorf("component not found") + } + status = results[0] + + m.s.Debugf("Found status for %v", purlName) + return &status, nil +} + +// GetProjectStatusByPurl gets only the project-level status (no version information) +func (m *ComponentStatusModel) GetProjectStatusByPurl(purlString string) (*ComponentProjectStatus, error) { + if len(purlString) == 0 { + m.s.Errorf("Please specify a valid Purl String to query") + return nil, errors.New("please specify a valid Purl String to query") + } + + purl, err := purlhelper.PurlFromString(purlString) + if err != nil { + return nil, err + } + + purlName, err := purlhelper.PurlNameFromString(purlString) + if err != nil { + return nil, err + } + + var status ComponentProjectStatus + + query := ` + SELECT DISTINCT + + p.purl_name, + p.component, + p.first_indexed_date, + p.last_indexed_date, + p.status, + p.status_change_date + FROM projects p + JOIN mines m ON p.mine_id = m.id + WHERE p.purl_name = $1 + AND m.purl_type = $2 + LIMIT 1 + ` + + var results []ComponentProjectStatus + err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type) + if err != nil { + m.s.Errorf("Failed to query project status for %v: %v", purlName, err) + return nil, fmt.Errorf("failed to query project status: %v", err) + } + if len(results) == 0 { + m.s.Warnf("No project status found for %v", purlName) + return nil, fmt.Errorf("component not found") + } + status = results[0] + + m.s.Debugf("Found project status for %v", purlName) + return &status, nil +} diff --git a/pkg/models/component_status_test.go b/pkg/models/component_status_test.go new file mode 100644 index 0000000..0ba990a --- /dev/null +++ b/pkg/models/component_status_test.go @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package models + +import ( + "context" + "fmt" + "testing" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" + _ "modernc.org/sqlite" + myconfig "scanoss.com/components/pkg/config" +) + +// TestGetComponentStatusByPurlAndVersion tests retrieving status for a specific component version +func TestGetComponentStatusByPurlAndVersion(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db := sqliteSetup(t) + defer CloseDB(db) + conn := sqliteConn(t, ctx, db) + defer CloseConn(conn) + err = LoadTestSQLData(db, ctx, conn) + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + componentStatusModel := NewComponentStatusModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + + // Test cases that should pass + passTestTable := []struct { + purl string + version string + }{ + { + purl: "pkg:npm/react", + version: "18.0.0", + }, + { + purl: "pkg:gem/tablestyle", + version: "0.1.0", + }, + } + + for _, test := range passTestTable { + fmt.Printf("Testing purl: %v, version: %v\n", test.purl, test.version) + status, err := componentStatusModel.GetComponentStatusByPurlAndVersion(test.purl, test.version) + if err != nil { + // It's ok if we don't find the specific version in test data + fmt.Printf("Version not found (expected): %v\n", err) + } else { + fmt.Printf("Status: %+v\n", status) + } + } + + // Test cases that should fail + failTestTable := []struct { + purl string + version string + }{ + { + purl: "", // Empty purl + version: "1.0.0", + }, + { + purl: "invalid-purl", // Invalid purl format + version: "1.0.0", + }, + } + + for _, test := range failTestTable { + _, err := componentStatusModel.GetComponentStatusByPurlAndVersion(test.purl, test.version) + if err == nil { + t.Errorf("An error was expected for purl: %v, version: %v", test.purl, test.version) + } + } +} + +// TestGetComponentStatusByPurl tests retrieving status for a component (without version) +func TestGetComponentStatusByPurl(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db := sqliteSetup(t) + defer CloseDB(db) + conn := sqliteConn(t, ctx, db) + defer CloseConn(conn) + err = LoadTestSQLData(db, ctx, conn) + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + componentStatusModel := NewComponentStatusModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + + // Test cases that should pass + passTestTable := []struct { + purl string + }{ + {purl: "pkg:npm/react"}, + {purl: "pkg:gem/tablestyle"}, + } + + for _, test := range passTestTable { + fmt.Printf("Testing purl: %v\n", test.purl) + status, err := componentStatusModel.GetComponentStatusByPurl(test.purl) + if err != nil { + fmt.Printf("Component not found (may be expected): %v\n", err) + } else { + fmt.Printf("Status: %+v\n", status) + } + } + + // Test cases that should fail + failTestTable := []struct { + purl string + }{ + {purl: ""}, // Empty purl + {purl: "invalid-purl"}, // Invalid purl format + {purl: "pkg:npm/NOEXIST"}, // Non-existent component + } + + for _, test := range failTestTable { + _, err := componentStatusModel.GetComponentStatusByPurl(test.purl) + if err == nil { + t.Errorf("An error was expected for purl: %v", test.purl) + } + } +} + +// TestGetProjectStatusByPurl tests retrieving project-level status only (no version info) +func TestGetProjectStatusByPurl(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db := sqliteSetup(t) + defer CloseDB(db) + conn := sqliteConn(t, ctx, db) + defer CloseConn(conn) + err = LoadTestSQLData(db, ctx, conn) + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + componentStatusModel := NewComponentStatusModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + + // Test cases that should pass + passTestTable := []struct { + purl string + }{ + {purl: "pkg:npm/react"}, + {purl: "pkg:gem/tablestyle"}, + } + + for _, test := range passTestTable { + fmt.Printf("Testing project status for purl: %v\n", test.purl) + status, err := componentStatusModel.GetProjectStatusByPurl(test.purl) + if err != nil { + fmt.Printf("Component not found (may be expected): %v\n", err) + } else { + fmt.Printf("Project Status: %+v\n", status) + if status.Component == "" { + t.Errorf("Expected non-empty component name for purl: %v", test.purl) + } + } + } + + // Test cases that should fail + failTestTable := []struct { + purl string + }{ + {purl: ""}, // Empty purl + {purl: "invalid-purl"}, // Invalid purl format + {purl: "pkg:npm/NOEXIST"}, // Non-existent component + } + + for _, test := range failTestTable { + _, err := componentStatusModel.GetProjectStatusByPurl(test.purl) + if err == nil { + t.Errorf("An error was expected for purl: %v", test.purl) + } + } +} diff --git a/pkg/models/tests/all_urls.sql b/pkg/models/tests/all_urls.sql index 51852e0..2f1b55d 100644 --- a/pkg/models/tests/all_urls.sql +++ b/pkg/models/tests/all_urls.sql @@ -13,11 +13,19 @@ CREATE TABLE all_urls version_id integer, license_id integer, purl_name text, + indexed_date text, + version_status text, + version_status_change_date text, primary key (package_hash, url, url_hash) ); -insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('4d66775f503b1e76582e7e5b2ea54d92', 'taballa.hp-PD', 'tablestyle', '0.0.10', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.10.gem', '5a088240b44efa142be4b3c40f8ae9c1', 1, 'MIT', 'tablestyle', 99999999, 5614); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('4d66775f503b1e76582e7e5b2ea54d92', 'taballa.hp-PD', 'tablestyle', '0.0.10', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.10.gem', '5a088240b44efa142be4b3c40f8ae9c1', 1, 'MIT', 'tablestyle', 99999999, 5614, '2013-08-26', 'active', '2013-08-26'); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('bfada11fd2b2b8fa23943b8b6fe5cb3f', 'taballa.hp-PD', 'tablestyle', '0.0.12', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.12.gem', '686dc352775b58652c9d9ddb2117f402', 1, 'MIT', 'tablestyle', 258510, 5614); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('react18001234567890abcdef1234567890', 'Jeff Barczewski', 'react', '18.0.0', '2022-03-29', 'https://registry.npmjs.org/react/-/react-18.0.0.tgz', 'react18001234567890abcdef12345678', 2, 'MIT', 'react', 10094162, 5614, '2022-03-29', 'active', '2022-03-29'); +-- Test versions with clean semver for component status tests +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('react199hash1234567890abcdef12345', 'Jeff Barczewski', 'react', '1.99.0', '2015-01-01', 'https://registry.npmjs.org/react/-/react-1.99.0.tgz', 'react199hash1234567890abcdef123', 2, 'MIT', 'react', 20000001, 5614, '2015-01-01', 'active', '2015-01-01'); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('react299hash1234567890abcdef12345', 'Jeff Barczewski', 'react', '2.99.0', '2016-01-01', 'https://registry.npmjs.org/react/-/react-2.99.0.tgz', 'react299hash1234567890abcdef123', 2, 'MIT', 'react', 20000002, 5614, '2016-01-01', 'active', '2016-01-01'); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('tablestyle099hash1234567890abcde', 'taballa.hp-PD', 'tablestyle', '0.99.0', '2013-07-10', 'https://rubygems.org/downloads/tablestyle-0.99.0.gem', 'tablestyle099hash1234567890abc', 1, 'MIT', 'tablestyle', 20000003, 5614, '2013-07-10', 'active', '2013-07-10'); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('f586d603a9cb2460c4517cffad6ad5e4', 'taballa.hp-PD', 'tablestyle', '0.0.7', null, 'https://rubygems.org/downloads/tablestyle-0.0.7.gem', '2a3251711e7010ca15d232ec4ec4fb16', 1, 'MIT', 'tablestyle', 3515237, 5614); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('b1cd1444c2f76e7564f57b0e047994a4', 'taballa.hp-PD', 'tablestyle', '0.0.4', '2013-07-08', 'https://rubygems.org/downloads/tablestyle-0.0.4.gem', 'b494b3e367d26b6ab2785ad3aee8afb7', 1, 'MIT', 'tablestyle', 10472506, 5614); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('952edab5837f5f0e59638dd791187725', 'taballa.hp-PD', 'tablestyle', '0.0.11', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.11.gem', '59fc4cf45a7d1425303a5bb897a463f4', 1, 'MIT', 'tablestyle', 11435747, 5614); diff --git a/pkg/models/tests/projects.sql b/pkg/models/tests/projects.sql index 967db2e..2b48947 100644 --- a/pkg/models/tests/projects.sql +++ b/pkg/models/tests/projects.sql @@ -615,9 +615,13 @@ CREATE TABLE projects verified text, license_id integer, git_license_id integer, + first_indexed_date text, + last_indexed_date text, + status text, + status_change_date text, PRIMARY KEY (mine_id, purl_name) ); -insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (1, 'taballa.hp-PD', 'tablestyle', '2013-07-05', '2013-08-26', 'MIT', 8, null, null, null, null, null, null, null, null, null, null, 'tablestyle', null, null, 5614, null); +insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id, first_indexed_date, last_indexed_date, status, status_change_date) values (1, 'taballa.hp-PD', 'tablestyle', '2013-07-05', '2013-08-26', 'MIT', 8, null, null, null, null, null, null, null, null, null, null, 'tablestyle', null, null, 5614, null, '2013-07-05', '2013-08-26', 'active', '2013-08-26'); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Sindre Sorhus', 'electron-debug', '2015-06-01', '2020-12-21', 'MIT', 28, 'sindresorhus', 'electron-debug', '2015-06-01', '2022-01-10', '2021-01-23', 693, 11, 55, 'MIT', 5, 'electron-debug', 'sindresorhus/electron-debug', '2022-01-11', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Shane Harris', 'node-blob', '2019-03-21', '2019-03-21', 'MIT', 2, 'shaneharris', 'node-blob', null, null, null, null, null, null, null, 5, 'node-blob', 'shaneharris/node-blob', '2021-05-24', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Gergely Hornich', 'sort-paths', '2016-09-03', '2018-08-18', 'MIT', 3, 'ghornich', 'sort-paths', null, null, null, null, null, null, null, 5, 'sort-paths', 'ghornich/sort-paths', '2021-05-24', 5614, null); @@ -634,7 +638,7 @@ insert into projects (mine_id, vendor, component, first_version_date, latest_ver insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Jake Zatecky', 'react-checkbox-tree', '2016-02-04', '2021-08-09', 'MIT', 47, 'jakezatecky', 'react-checkbox-tree', '2016-02-04', '2022-01-09', '2022-01-03', 552, 80, 165, 'MIT', 5, 'react-checkbox-tree', 'jakezatecky/react-checkbox-tree', '2022-01-11', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'React Training', 'history', '2012-04-29', '2021-12-17', 'MIT', 99, 'ReactTraining', 'history', '2015-07-18', '2021-08-12', '2021-08-12', 7099, 115, 841, 'MIT', 5, 'history', 'reacttraining/history', '2021-08-12', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Nikhil Marathe', 'uuid', '2011-03-31', '2021-11-29', 'MIT', 34, 'uuidjs', 'uuid', '2010-12-28', '2022-01-11', '2022-01-04', 11878, 20, 797, 'MIT', 5, 'uuid', 'uuidjs/uuid', '2022-01-11', 5614, null); -insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Jeff Barczewski', 'react', '2011-10-26', '2021-12-28', 'MIT', 739, 'facebook', 'react', '2013-05-24', '2022-01-12', '2022-01-11', 180572, 915, 36701, 'MIT', 5, 'react', 'facebook/react', '2022-01-11', 5614, null); +insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id, first_indexed_date, last_indexed_date, status, status_change_date) values (2, 'Jeff Barczewski', 'react', '2011-10-26', '2021-12-28', 'MIT', 739, 'facebook', 'react', '2013-05-24', '2022-01-12', '2022-01-11', 180572, 915, 36701, 'MIT', 5, 'react', 'facebook/react', '2022-01-11', 5614, null, '2011-10-26', '2022-01-11', 'active', '2022-01-11'); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'React Training', 'react-router-dom', '2016-12-14', '2021-12-17', 'MIT', 64, 'ReactTraining', 'react-router', '2014-05-16', '2021-08-12', '2021-08-11', 43727, 59, 8505, 'MIT', 5, 'react-router-dom', 'reacttraining/react-router', '2021-08-12', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Felix Geisendörfer', 'form-data', '2011-05-16', '2021-02-15', 'MIT', 38, 'form-data', 'form-data', '2011-05-16', '2022-01-11', '2021-11-30', 1962, 111, 256, 'MIT', 5, 'form-data', 'form-data/form-data', '2022-01-11', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Toru Nagashima', 'abort-controller', '2017-09-29', '2019-03-30', 'MIT', 11, 'mysticatea', 'abort-controller', '2017-09-29', '2021-12-30', '2021-03-30', 258, 17, 25, 'MIT', 5, 'abort-controller', 'mysticatea/abort-controller', '2022-01-11', 5614, null); diff --git a/pkg/models/tests/versions.sql b/pkg/models/tests/versions.sql index 3952288..4207dcc 100644 --- a/pkg/models/tests/versions.sql +++ b/pkg/models/tests/versions.sql @@ -237,6 +237,10 @@ insert into versions (id, version_name, semver) values (11766524, '0.5.14', 'v0. insert into versions (id, version_name, semver) values (6854887, '18.0.0-alpha-02f411578-20211019', 'v18.0.0-alpha-02f411578-20211019'); insert into versions (id, version_name, semver) values (12617765, '0.10.3', ''); insert into versions (id, version_name, semver) values (6924596, '1.38.0', ''); +-- Test versions with clean semver for component status tests +insert into versions (id, version_name, semver) values (20000001, '1.99.0', 'v1.99.0'); +insert into versions (id, version_name, semver) values (20000002, '2.99.0', 'v2.99.0'); +insert into versions (id, version_name, semver) values (20000003, '0.99.0', 'v0.99.0'); insert into versions (id, version_name, semver) values (11401945, '1.0.0rc1', 'v1.0.0-rc1'); insert into versions (id, version_name, semver) values (3922891, '3.4.0', ''); insert into versions (id, version_name, semver) values (1318167, '13.5.1', ''); diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index ab08a16..f7db794 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -76,7 +76,7 @@ func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompS } // Search the KB for information about the components - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace)) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) dtoComponents, err := compUc.SearchComponents(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -126,7 +126,7 @@ func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.C return &pb.CompVersionResponse{Status: status}, nil } // Creates the use case - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace)) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) dtoOutput, err := compUc.GetComponentVersions(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -171,6 +171,98 @@ func telemetryCompVersionRequestTime(ctx context.Context, config *myconfig.Serve } } +// GetComponentStatus retrieves status information for a specific component +func (d componentServer) GetComponentStatus(ctx context.Context, request *common.ComponentRequest) (*pb.ComponentStatusResponse, error) { + s := ctxzap.Extract(ctx).Sugar() + s.Info("Processing component status request...") + + // Verify the input request + if len(request.Purl) == 0 { + s.Error("No purl supplied") + return &pb.ComponentStatusResponse{}, se.NewBadRequestError("No purl supplied", nil) + } + + // Convert the request to internal DTO + dtoRequest, err := convertComponentStatusInput(s, request) + if err != nil { + s.Errorf("Failed to convert component status input: %v", err) + return &pb.ComponentStatusResponse{}, err + } + + // Create the use case + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) + dtoOutput, err := compUc.GetComponentStatus(dtoRequest) + if err != nil { + s.Errorf("Failed to get component status: %v", err) + return &pb.ComponentStatusResponse{}, err + } + + // Convert the output to protobuf + statusResponse, err := convertComponentStatusOutput(s, dtoOutput) + if err != nil { + s.Errorf("Failed to convert component status output: %v", err) + return &pb.ComponentStatusResponse{}, errors.New("problems encountered extracting component status data") + } + + return statusResponse, nil +} + +// GetComponentsStatus retrieves status information for multiple components +func (d componentServer) GetComponentsStatus(ctx context.Context, request *common.ComponentsRequest) (*pb.ComponentsStatusResponse, error) { + s := ctxzap.Extract(ctx).Sugar() + s.Info("Processing components status request...") + + // Verify the input request + if len(request.Components) == 0 { + status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No components supplied", nil)) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.ComponentsStatusResponse{Status: status}, nil + } + + // Convert the request to internal DTO + dtoRequest, err := convertComponentsStatusInput(s, request) + if err != nil { + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.ComponentsStatusResponse{Status: status}, nil + } + + // Create the use case + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) + dtoOutput, err := compUc.GetComponentsStatus(dtoRequest) + if err != nil { + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.ComponentsStatusResponse{Status: status}, nil + } + + // Convert the output to protobuf + statusResponse, err := convertComponentsStatusOutput(s, dtoOutput) + if err != nil { + s.Errorf("Failed to convert components status output: %v", err) + return &pb.ComponentsStatusResponse{Status: &common.StatusResponse{ + Status: common.StatusCode_FAILED, + Message: "Problems encountered extracting components status data", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }}, nil + } + + // Set the status and respond with the data + return &pb.ComponentsStatusResponse{ + Components: statusResponse.Components, + Status: &common.StatusResponse{ + Status: common.StatusCode_SUCCESS, + Message: "Success", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }, + }, nil +} + // getDBVersion fetches the database version from the db_version table. // Returns nil if the table doesn't exist or query fails (backward compatibility). func (d componentServer) getDBVersion() *common.StatusResponse_DB { diff --git a/pkg/service/component_service_test.go b/pkg/service/component_service_test.go index a5452fb..8e5600c 100644 --- a/pkg/service/component_service_test.go +++ b/pkg/service/component_service_test.go @@ -246,3 +246,80 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { }) } } + +func TestComponentServer_GetComponentStatus(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.App.Version = "test-version" + s := NewComponentServer(db, myConfig) + + type args struct { + ctx context.Context + req *common.ComponentRequest + } + tests := []struct { + name string + s pb.ComponentsServer + args args + wantErr bool + checkFn func(*testing.T, *pb.ComponentStatusResponse) + }{ + { + name: "Get status for react npm package", + s: s, + args: args{ + ctx: ctx, + req: &common.ComponentRequest{ + Purl: "pkg:npm/react", + Requirement: "^18.0.0", + }, + }, + wantErr: false, + checkFn: func(t *testing.T, resp *pb.ComponentStatusResponse) { + if resp == nil { + t.Error("Expected non-nil response") + } + }, + }, + { + name: "Get status with empty purl", + s: s, + args: args{ + ctx: ctx, + req: &common.ComponentRequest{}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.s.GetComponentStatus(tt.args.ctx, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("service.GetComponentStatus() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.checkFn != nil && err == nil { + tt.checkFn(t, got) + } + }) + } +} diff --git a/pkg/service/component_support.go b/pkg/service/component_support.go index 3eb1485..9c0663e 100644 --- a/pkg/service/component_support.go +++ b/pkg/service/component_support.go @@ -3,6 +3,8 @@ package service import ( "encoding/json" "errors" + + "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" pb "github.com/scanoss/papi/api/componentsv2" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" @@ -79,3 +81,85 @@ func convertCompVersionsOutput(s *zap.SugaredLogger, output dtos.ComponentVersio } return &compResp, nil } + +func convertComponentStatusInput(s *zap.SugaredLogger, request interface{}) (dtos.ComponentStatusInput, error) { + data, err := json.Marshal(request) + if err != nil { + return dtos.ComponentStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + dtoRequest, err := dtos.ParseComponentStatusInput(s, data) + if err != nil { + return dtos.ComponentStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + return dtoRequest, nil +} + +func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentStatusOutput) (*pb.ComponentStatusResponse, error) { + + response := &pb.ComponentStatusResponse{ + Purl: output.Purl, + Requirement: output.Requirement, + } + if output.ComponentStatus.ErrorCode == nil { + response.ComponentStatus = &pb.ComponentStatusResponse_ComponentStatus{ + FirstIndexedDate: &output.ComponentStatus.FirstIndexedDate, + LastIndexedDate: &output.ComponentStatus.LastIndexedDate, + Status: &output.ComponentStatus.Status, + RepositoryStatus: &output.ComponentStatus.RepositoryStatus, + } + if output.ComponentStatus.StatusChangeDate != "" { + response.ComponentStatus.StatusChangeDate = &output.ComponentStatus.StatusChangeDate + } + response.Name = output.Name + } else { + response.ComponentStatus = &pb.ComponentStatusResponse_ComponentStatus{ + ErrorMessage: output.ComponentStatus.ErrorMessage, + ErrorCode: domain.StatusCodeToErrorCode(*output.ComponentStatus.ErrorCode), + } + return response, nil + } + if output.VersionStatus != nil { + if output.VersionStatus.ErrorCode == nil { + response.VersionStatus = &pb.ComponentStatusResponse_VersionStatus{ + Version: output.VersionStatus.Version, + RepositoryStatus: &output.VersionStatus.RepositoryStatus, + Status: &output.VersionStatus.Status, + IndexedDate: &output.VersionStatus.IndexedDate, + } + if output.VersionStatus.StatusChangeDate != "" { + response.VersionStatus.StatusChangeDate = &output.VersionStatus.StatusChangeDate + } + } else { + response.VersionStatus = &pb.ComponentStatusResponse_VersionStatus{ + Version: output.VersionStatus.Version, + ErrorMessage: output.VersionStatus.ErrorMessage, + ErrorCode: domain.StatusCodeToErrorCode(*output.VersionStatus.ErrorCode), + } + } + } + + return response, nil +} + +func convertComponentsStatusInput(s *zap.SugaredLogger, request interface{}) (dtos.ComponentsStatusInput, error) { + data, err := json.Marshal(request) + if err != nil { + return dtos.ComponentsStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + dtoRequest, err := dtos.ParseComponentsStatusInput(s, data) + if err != nil { + return dtos.ComponentsStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + return dtoRequest, nil +} + +func convertComponentsStatusOutput(s *zap.SugaredLogger, output dtos.ComponentsStatusOutput) (*pb.ComponentsStatusResponse, error) { + + var statusResp pb.ComponentsStatusResponse + for _, c := range output.Components { + cs, _ := convertComponentStatusOutput(s, c) + statusResp.Components = append(statusResp.Components, cs) + } + + return &statusResp, nil +} diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index 88cffe3..da5f956 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -20,10 +20,14 @@ import ( "context" "errors" "fmt" + se "scanoss.com/components/pkg/errors" "github.com/jmoiron/sqlx" + cmpHelper "github.com/scanoss/go-component-helper/componenthelper" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" purlhelper "github.com/scanoss/go-purl-helper/pkg" "go.uber.org/zap" "scanoss.com/components/pkg/dtos" @@ -31,17 +35,23 @@ import ( ) type ComponentUseCase struct { - ctx context.Context - s *zap.SugaredLogger - q *database.DBQueryContext - components *models.ComponentModel - allUrl *models.AllUrlsModel + ctx context.Context + s *zap.SugaredLogger + q *database.DBQueryContext + components *models.ComponentModel + allUrl *models.AllUrlsModel + componentStatus *models.ComponentStatusModel + db *sqlx.DB + statusMapper *StatusMapper } -func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext) *ComponentUseCase { +func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext, statusMappingJSON string) *ComponentUseCase { return &ComponentUseCase{ctx: ctx, s: s, q: q, - components: models.NewComponentModel(ctx, s, q, database.GetLikeOperator(db)), - allUrl: models.NewAllUrlModel(ctx, s, q), + components: models.NewComponentModel(ctx, s, q, database.GetLikeOperator(db)), + allUrl: models.NewAllUrlModel(ctx, s, q), + componentStatus: models.NewComponentStatusModel(ctx, s, q), + db: db, + statusMapper: NewStatusMapper(s, statusMappingJSON), } } @@ -141,3 +151,150 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp } return dtos.ComponentVersionsOutput{Component: output}, nil } + +func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) (dtos.ComponentStatusOutput, error) { + if len(request.Purl) == 0 { + c.s.Errorf("The request does not contain purl to retrieve component status") + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) + } + + results := cmpHelper.GetComponentsVersion(cmpHelper.ComponentVersionCfg{ + MaxWorkers: 1, + Ctx: c.ctx, + S: c.s, + DB: c.db, + Input: []cmpHelper.ComponentDTO{ + {Purl: request.Purl, Requirement: request.Requirement}, + }, + }) + + if len(results) > 0 { + // Component and Version exists or requirement met + if results[0].Status.StatusCode == domain.Success { + statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(results[0].Purl) + if errComp != nil { + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving Component level data", errors.New("Error retrieving Component Level Data")) + } + + var statusVersion *models.ComponentVersionStatus + var errVersion error + if len(results[0].Version) > 0 { + statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(request.Purl, results[0].Version) + } else if len(results[0].Requirement) > 0 { + statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(request.Purl, results[0].Requirement) + } + if errVersion != nil { + c.s.Warnf("Problems getting version level status data for: %v - %v", request.Purl, errVersion) + } + + output := dtos.ComponentStatusOutput{ + Purl: request.Purl, + Name: statComponent.Component, + Requirement: request.Requirement, + VersionStatus: &dtos.VersionStatusOutput{ + Version: statusVersion.Version, + Status: c.statusMapper.MapStatus(statusVersion.VersionStatus.String), + RepositoryStatus: statusVersion.VersionStatus.String, + IndexedDate: statusVersion.IndexedDate.String, + }, + ComponentStatus: &dtos.ComponentStatusInfo{ + Status: c.statusMapper.MapStatus(statComponent.Status.String), + RepositoryStatus: statComponent.Status.String, + FirstIndexedDate: statComponent.FirstIndexedDate.String, + LastIndexedDate: statComponent.LastIndexedDate.String, + }, + } + if statusVersion.VersionStatusChangeDate.String != "" { + output.VersionStatus.StatusChangeDate = statusVersion.VersionStatusChangeDate.String + } + + if statComponent.StatusChangeDate.String != "" { + output.ComponentStatus.StatusChangeDate = statComponent.StatusChangeDate.String + } + + return output, nil + + } else if results[0].Status.StatusCode == domain.VersionNotFound { // Valid component but VERSION NOT FOUND + statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(results[0].Purl) + if errComp != nil { + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving information", errors.New("Error retrieving information")) + } + output := dtos.ComponentStatusOutput{ + Purl: request.Purl, + Name: statComponent.Component, + Requirement: request.Requirement, + VersionStatus: &dtos.VersionStatusOutput{ + Version: request.Requirement, + ErrorMessage: &results[0].Status.Message, + ErrorCode: &results[0].Status.StatusCode, + }, + ComponentStatus: &dtos.ComponentStatusInfo{Status: c.statusMapper.MapStatus(statComponent.Status.String), + RepositoryStatus: statComponent.Status.String, + FirstIndexedDate: statComponent.FirstIndexedDate.String, + LastIndexedDate: statComponent.LastIndexedDate.String, + }, + } + return output, nil + } else if results[0].Status.StatusCode == domain.InvalidPurl { // The requested purl is invalid, minimun data retrieved + errorStatus := dtos.ComponentStatusOutput{ + Purl: results[0].Purl, + Name: "", + Requirement: results[0].Requirement, + ComponentStatus: &dtos.ComponentStatusInfo{ + ErrorMessage: &results[0].Status.Message, + ErrorCode: &results[0].Status.StatusCode, + }, + } + return errorStatus, nil + + } else if results[0].Status.StatusCode == domain.ComponentNotFound { // Component not found on DB, minimun data retrieved + errorStatus := dtos.ComponentStatusOutput{ + Purl: results[0].Purl, + Name: "", + Requirement: results[0].Requirement, + ComponentStatus: &dtos.ComponentStatusInfo{ + ErrorMessage: &results[0].Status.Message, + ErrorCode: &results[0].Status.StatusCode, + }, + } + return errorStatus, nil + + } + } + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) +} + +func (c ComponentUseCase) GetComponentsStatus(request dtos.ComponentsStatusInput) (dtos.ComponentsStatusOutput, error) { + if len(request.Components) == 0 { + c.s.Errorf("The request does not contain any components to retrieve status") + return dtos.ComponentsStatusOutput{}, se.NewBadRequestError("components array is required", errors.New("components array is required")) + } + + var output dtos.ComponentsStatusOutput + output.Components = make([]dtos.ComponentStatusOutput, 0, len(request.Components)) + + // Process each component request + for _, componentRequest := range request.Components { + componentStatus, err := c.GetComponentStatus(componentRequest) + if err != nil { + // For batch requests, we continue even if one component fails + // Add an error entry for this component + c.s.Warnf("Failed to get status for component: %v - %v", componentRequest.Purl, err) + + errorMsg := err.Error() + errorStatus := dtos.ComponentStatusOutput{ + Purl: componentRequest.Purl, + Name: "", + Requirement: componentRequest.Requirement, + ComponentStatus: &dtos.ComponentStatusInfo{ + ErrorMessage: dtos.StringPtr(errorMsg), + }, + } + output.Components = append(output.Components, errorStatus) + } else { + output.Components = append(output.Components, componentStatus) + } + } + + return output, nil +} diff --git a/pkg/usecase/component_test.go b/pkg/usecase/component_test.go index 5423fdc..8b11b13 100644 --- a/pkg/usecase/component_test.go +++ b/pkg/usecase/component_test.go @@ -54,7 +54,7 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { } myConfig.Database.Trace = true - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace)) + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") goodTable := []dtos.ComponentSearchInput{ { @@ -111,7 +111,7 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { t.Fatalf("failed to load Config: %v", err) } - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace)) + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") goodTable := []dtos.ComponentVersionsInput{ { @@ -151,3 +151,279 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { } } + +func TestComponentUseCase_GetComponentStatus(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + + // Good test cases + goodTable := []dtos.ComponentStatusInput{ + { + Purl: "pkg:npm/react", + Requirement: "^18.0.0", + }, + { + Purl: "pkg:gem/tablestyle", + Requirement: ">=0.1.0", + }, + } + + for i, input := range goodTable { + statusOut, err := compUc.GetComponentStatus(input) + if err != nil { + // It's ok if component is not found in test data + fmt.Printf("test case %d: Component status not found (may be expected): %v\n", i, err) + } else { + fmt.Printf("Status response: %+v\n", statusOut) + if statusOut.Purl != input.Purl { + t.Errorf("Expected purl %v, got %v", input.Purl, statusOut.Purl) + } + } + } + + // Fail test cases + failTestTable := []dtos.ComponentStatusInput{ + { + Purl: "", // Empty purl + Requirement: "1.0.0", + }, + } + + for i, input := range failTestTable { + _, err := compUc.GetComponentStatus(input) + if err == nil { + t.Errorf("test case %d: an error was expected for input %+v", i, input) + } + } +} + +func TestComponentUseCase_GetComponentsStatus(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + + // Test with multiple components + multipleInput := dtos.ComponentsStatusInput{ + Components: []dtos.ComponentStatusInput{ + { + Purl: "pkg:npm/react", + Requirement: "^18.0.0", + }, + { + Purl: "pkg:gem/tablestyle", + Requirement: ">=0.1.0", + }, + { + Purl: "", // This should fail + Requirement: "1.0.0", + }, + }, + } + + statusOut, err := compUc.GetComponentsStatus(multipleInput) + if err != nil { + t.Fatalf("Unexpected error getting components status: %v", err) + } + + if len(statusOut.Components) != 3 { + t.Errorf("Expected 3 component statuses, got %d", len(statusOut.Components)) + } + + fmt.Printf("Components Status response: %+v\n", statusOut) + + // Test with empty components array + emptyInput := dtos.ComponentsStatusInput{ + Components: []dtos.ComponentStatusInput{}, + } + + _, err = compUc.GetComponentsStatus(emptyInput) + if err == nil { + t.Errorf("Expected error for empty components array") + } +} + +// TestComponentUseCase_GetComponentStatus_AllCases tests all status code paths +func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + + testCases := []struct { + name string + input dtos.ComponentStatusInput + expectError bool + expectedPurl string + checkStatusCode bool + statusShouldPass bool // true = Success, false = error status + }{ + { + name: "Success - Component and version found (react 1.99.0)", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/react", + Requirement: "1.99.0", + }, + expectError: false, + expectedPurl: "pkg:npm/react", + checkStatusCode: true, + statusShouldPass: true, + }, + { + name: "Success - Component and version found with caret (react ^1.99.0)", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/react", + Requirement: "^1.99.0", + }, + expectError: false, + expectedPurl: "pkg:npm/react", + checkStatusCode: true, + statusShouldPass: true, + }, + { + name: "Success - Component and version found (tablestyle 0.99.0)", + input: dtos.ComponentStatusInput{ + Purl: "pkg:gem/tablestyle", + Requirement: "0.99.0", + }, + expectError: false, + expectedPurl: "pkg:gem/tablestyle", + checkStatusCode: true, + statusShouldPass: true, + }, + { + name: "VersionNotFound - Component exists but version doesn't", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/react", + Requirement: "999.0.0", + }, + expectError: false, + expectedPurl: "pkg:npm/react", + checkStatusCode: true, + statusShouldPass: false, + }, + { + name: "ComponentNotFound - Component doesn't exist", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/nonexistent-package-xyz-123", + Requirement: "1.0.0", + }, + expectError: false, + expectedPurl: "pkg:npm/nonexistent-package-xyz-123", + checkStatusCode: true, + statusShouldPass: false, + }, + { + name: "InvalidPurl - Malformed purl", + input: dtos.ComponentStatusInput{ + Purl: "invalid-purl-format", + Requirement: "1.0.0", + }, + expectError: false, + checkStatusCode: true, + statusShouldPass: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + statusOut, err := compUc.GetComponentStatus(tc.input) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if tc.expectedPurl != "" && statusOut.Purl != tc.expectedPurl { + t.Errorf("Expected purl %v, got %v", tc.expectedPurl, statusOut.Purl) + } + + if tc.statusShouldPass { + // For successful status, we should have component info + if statusOut.Name == "" { + t.Errorf("Expected non-empty component name for success case") + } + if statusOut.ComponentStatus == nil { + t.Errorf("Expected ComponentStatus for success case") + } + fmt.Printf("✓ %s: Success status received\n", tc.name) + } else { + // For error status, we should have error info + if statusOut.ComponentStatus != nil && statusOut.ComponentStatus.ErrorMessage == nil && statusOut.VersionStatus != nil && statusOut.VersionStatus.ErrorMessage == nil { + t.Errorf("Expected error message for failure case") + } + fmt.Printf("✓ %s: Error status received as expected\n", tc.name) + } + }) + } +} + diff --git a/pkg/usecase/status_mapper.go b/pkg/usecase/status_mapper.go new file mode 100644 index 0000000..0d8bfd0 --- /dev/null +++ b/pkg/usecase/status_mapper.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package usecase + +import ( + "encoding/json" + "strings" + + "go.uber.org/zap" +) + +// StatusMapper handles mapping of database statuses to classified statuses +type StatusMapper struct { + mapping map[string]string + s *zap.SugaredLogger +} + +// NewStatusMapper creates a new StatusMapper with the provided mapping JSON string +// If mappingJSON is empty or invalid, it uses default mappings +func NewStatusMapper(s *zap.SugaredLogger, mappingJSON string) *StatusMapper { + mapper := &StatusMapper{ + s: s, + mapping: getDefaultStatusMapping(), + } + + // If custom mapping provided, try to parse it + if len(strings.TrimSpace(mappingJSON)) > 0 { + var customMapping map[string]string + err := json.Unmarshal([]byte(mappingJSON), &customMapping) + if err != nil { + s.Warnf("Failed to parse STATUS_MAPPING JSON, using defaults: %v", err) + } else { + // Merge custom mapping with defaults (custom overrides defaults) + for key, value := range customMapping { + mapper.mapping[strings.ToLower(key)] = value + } + s.Infof("Loaded custom status mapping with %d entries", len(customMapping)) + } + } + + return mapper +} + +// MapStatus maps a database status to its classified status +// Returns the mapped status, or the original if no mapping exists +func (m *StatusMapper) MapStatus(dbStatus string) string { + if dbStatus == "" { + return "" + } + + // Normalize to lowercase for lookup + normalized := strings.ToLower(strings.TrimSpace(dbStatus)) + + if mapped, exists := m.mapping[normalized]; exists { + return mapped + } + + // If no mapping exists, return original value + return dbStatus +} + +// getDefaultStatusMapping returns the default status classification mapping +func getDefaultStatusMapping() map[string]string { + return map[string]string{ + "active": "active", + "unlisted": "removed", + "yanked": "removed", + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": "removed", + "archived": "deprecated", + } +} diff --git a/pkg/usecase/status_mapper_test.go b/pkg/usecase/status_mapper_test.go new file mode 100644 index 0000000..22524e6 --- /dev/null +++ b/pkg/usecase/status_mapper_test.go @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package usecase + +import ( + "testing" + + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" +) + +func TestStatusMapper_MapStatus_DefaultMappings(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with empty JSON (should use defaults) + mapper := NewStatusMapper(s, "") + + testCases := []struct { + name string + input string + expected string + }{ + {"active maps to active", "active", "active"}, + {"unlisted maps to removed", "unlisted", "removed"}, + {"yanked maps to removed", "yanked", "removed"}, + {"deleted maps to deleted", "deleted", "deleted"}, + {"deprecated maps to deprecated", "deprecated", "deprecated"}, + {"unpublished maps to removed", "unpublished", "removed"}, + {"archived maps to deprecated", "archived", "deprecated"}, + {"ACTIVE (uppercase) maps to active", "ACTIVE", "active"}, + {"Unlisted (mixed case) maps to removed", "Unlisted", "removed"}, + {"unknown status returns original", "unknown-status", "unknown-status"}, + {"empty string returns empty", "", ""}, + {"whitespace status returns original", " some status ", " some status "}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q", tc.input, result, tc.expected) + } + }) + } +} + +func TestStatusMapper_MapStatus_CustomMappings(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with custom JSON mappings + customJSON := `{"unlisted":"custom-removed","new-status":"custom-value","active":"still-active"}` + mapper := NewStatusMapper(s, customJSON) + + testCases := []struct { + name string + input string + expected string + }{ + {"custom unlisted mapping", "unlisted", "custom-removed"}, + {"custom new-status mapping", "new-status", "custom-value"}, + {"custom active override", "active", "still-active"}, + {"default yanked still works", "yanked", "removed"}, + {"default deleted still works", "deleted", "deleted"}, + {"unknown status returns original", "completely-unknown", "completely-unknown"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q", tc.input, result, tc.expected) + } + }) + } +} + +func TestStatusMapper_MapStatus_InvalidJSON(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with invalid JSON (should fall back to defaults) + invalidJSON := `{this is not valid json}` + mapper := NewStatusMapper(s, invalidJSON) + + // Should use defaults when JSON is invalid + result := mapper.MapStatus("unlisted") + if result != "removed" { + t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, "removed") + } + + result = mapper.MapStatus("active") + if result != "active" { + t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, "active") + } +} + +func TestStatusMapper_MapStatus_EmptyJSON(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Test various empty JSON scenarios + emptyScenarios := []string{"", " ", "{}", " {} "} + + for _, emptyJSON := range emptyScenarios { + mapper := NewStatusMapper(s, emptyJSON) + + // Should use defaults + result := mapper.MapStatus("unlisted") + if result != "removed" { + t.Errorf("MapStatus with empty JSON %q should use defaults, got %q, expected %q", emptyJSON, result, "removed") + } + } +} + +func TestStatusMapper_MapStatus_CaseSensitivity(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + mapper := NewStatusMapper(s, "") + + // Test that mapping is case-insensitive for lookup + testCases := []struct { + input string + expected string + }{ + {"active", "active"}, + {"ACTIVE", "active"}, + {"Active", "active"}, + {"AcTiVe", "active"}, + {"unlisted", "removed"}, + {"UNLISTED", "removed"}, + {"Unlisted", "removed"}, + {"UnLiStEd", "removed"}, + } + + for _, tc := range testCases { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q (case-insensitive)", tc.input, result, tc.expected) + } + } +} + +func TestGetDefaultStatusMapping(t *testing.T) { + defaults := getDefaultStatusMapping() + + expectedMappings := map[string]string{ + "active": "active", + "unlisted": "removed", + "yanked": "removed", + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": "removed", + "archived": "deprecated", + } + + if len(defaults) != len(expectedMappings) { + t.Errorf("Expected %d default mappings, got %d", len(expectedMappings), len(defaults)) + } + + for key, expectedValue := range expectedMappings { + if actualValue, exists := defaults[key]; !exists { + t.Errorf("Default mapping missing key %q", key) + } else if actualValue != expectedValue { + t.Errorf("Default mapping for %q: got %q, expected %q", key, actualValue, expectedValue) + } + } +} From 857153a817fbfe9ddbbda95f16e2f8c40ccfe70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Go=C3=B1i?= Date: Thu, 19 Mar 2026 15:01:00 -0300 Subject: [PATCH 02/13] feat(status) Adds GetComponentStatus/GetComponentsStatus endpoint for retrieving dev life-cycle. Added a mapping scheme for custom status names --- CHANGELOG.md | 11 +- README.md | 5 + config/app-config-dev.json | 11 ++ go.mod | 12 +- go.sum | 10 +- pkg/config/server_config.go | 15 +- pkg/config/status_mapper.go | 134 ++++++++++++++++++ pkg/{usecase => config}/status_mapper_test.go | 86 ++++++++++- pkg/models/tests/all_urls.sql | 8 ++ pkg/service/component_service.go | 8 +- pkg/service/component_support.go | 18 +-- pkg/usecase/component.go | 16 ++- pkg/usecase/component_test.go | 33 ++--- pkg/usecase/status_mapper.go | 87 ------------ 14 files changed, 318 insertions(+), 136 deletions(-) create mode 100644 pkg/config/status_mapper.go rename pkg/{usecase => config}/status_mapper_test.go (70%) delete mode 100644 pkg/usecase/status_mapper.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d6cecd2..2552564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Upcoming changes... + +## [0.8.0] - 2026-03-20 +### Added +- Added `GetComponentStatus`/`GetComponentsStatus` services for getting single and multiple development life-cycle information +- Added support for custom status mapping for _retrieved_ and _registry_ specific status +### Changed +- Using **go-component-helper** to get always the right component version based on user request + + ## [0.7.0] - 2026-01-30 ### Added - Added database version info (`schema_version`, `created_at`) to `StatusResponse` across all component service endpoints @@ -38,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.1] - ? ### Added - ? - +[0.8.0]: https://github.com/scanoss/components/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/scanoss/components/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/scanoss/components/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/scanoss/components/compare/v0.4.0...v0.5.0 diff --git a/README.md b/README.md index 1765f4d..0537541 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,12 @@ DB_SCHEMA=scanoss DB_SSL_MODE=disable DB_DSN= ``` +## Satus mapping +User can define custom status mapping using `STATUS_MAPPING` variable (JSON format) +``` bash +STATUS_MAPPING='{"unlisted":"removed","yanked":"removed","deleted":"deleted","deprecated":"deprecated","unpublished":"removed","archived":"deprecated","active":"active"}' +``` ## Docker Environment diff --git a/config/app-config-dev.json b/config/app-config-dev.json index 4cb5440..5f715f8 100644 --- a/config/app-config-dev.json +++ b/config/app-config-dev.json @@ -10,5 +10,16 @@ "User": "scanoss", "Passwd": "secret123!", "Schema": "scanoss" + }, + "StatusMapping": { + "Mapping": { + "unlisted": "removed", + "yanked": "removed", + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": "removed", + "archived": "deprecated", + "active": "active" + } } } diff --git a/go.mod b/go.mod index 829dd0a..c2b2a7b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module scanoss.com/components -go 1.24.0 +go 1.24.4 toolchain go1.24.6 @@ -10,11 +10,11 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.11.2 - github.com/scanoss/go-component-helper v0.1.0 + github.com/scanoss/go-component-helper v0.4.0 github.com/scanoss/go-grpc-helper v0.13.0 - github.com/scanoss/go-models v0.5.1 + github.com/scanoss/go-models v0.7.0 github.com/scanoss/go-purl-helper v0.2.1 - github.com/scanoss/papi v0.31.0 + github.com/scanoss/papi v0.32.1 github.com/scanoss/zap-logging-helper v0.4.0 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/metric v1.40.0 @@ -72,9 +72,9 @@ require ( // Details of how to use the "replace" command for local development // https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive // ie. -replace github.com/scanoss/papi => ../papi +//replace github.com/scanoss/papi => ../papi -//replace github.com/scanoss/go-component-helper => ../go-component-helper +//replace github.com/scanoss/go-component-helper/componenthelper => ../go-component-helper // require github.com/scanoss/papi v0.0.0-unpublished //replace github.com/scanoss/go-grpc-helper v0.6.0 => ../go-grpc-helper diff --git a/go.sum b/go.sum index c7a1331..ce0ed6a 100644 --- a/go.sum +++ b/go.sum @@ -624,16 +624,18 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/scanoss/go-component-helper v0.1.0 h1:lybXLkWnpTTZ/XQecUXRmZXCaYnpUQ/qU3xHOwNWOAs= -github.com/scanoss/go-component-helper v0.1.0/go.mod h1:JrkReO2ODXkc0N6CmGGdosQRgfxRZCCwPHxSWkVaruY= +github.com/scanoss/go-component-helper v0.4.0 h1:LrO7LPY8m9mEnaG4lKb6ssT410nFjEmm5EJPEaYRWHw= +github.com/scanoss/go-component-helper v0.4.0/go.mod h1:VWOGsu3JWI/NT53w7rOWL/yN/2jfwsEHdFqMvIH8TRI= github.com/scanoss/go-grpc-helper v0.13.0 h1:GtKDKuc2jTtv27naIVF3f095tuJvSvxbeN+HmUdBS4Y= github.com/scanoss/go-grpc-helper v0.13.0/go.mod h1:mNfM/788jzo8rQ2K2Y97UYQPd6MjuVXEM7r05kWPOJc= -github.com/scanoss/go-models v0.5.1 h1:MdHRhQcjG6sz5BLbyWfu/+jfH7I4Snnpo9XriukB7GI= -github.com/scanoss/go-models v0.5.1/go.mod h1:7r2ylOddgwMx8uAf7dvYBiThiU5eeVwHVvVgsVHtn3Q= +github.com/scanoss/go-models v0.7.0 h1:6uhr3sGWIFz3wb1k+nhFfEoGbR8n5e5q3kJGJuLqdPc= +github.com/scanoss/go-models v0.7.0/go.mod h1:gkXP2/3ZUn8IyH/z/528aTcWZ2mzlzsXiY4bzBbrM+o= github.com/scanoss/go-purl-helper v0.2.1 h1:jp960a585ycyJSlqZky1NatMJBIQi/JGITDfNSu/9As= github.com/scanoss/go-purl-helper v0.2.1/go.mod h1:v20/bKD8G+vGrILdiq6r0hyRD2bO8frCJlu9drEcQ38= github.com/scanoss/ipfilter/v2 v2.0.2 h1:GaB9i8kVJg9JQZm5XGStYkEpiaCVdsrj7ezI2wV/oh8= github.com/scanoss/ipfilter/v2 v2.0.2/go.mod h1:AwrpX4XGbZ7EKISMi1d6E5csBk1nWB8+ugpvXHFcTpA= +github.com/scanoss/papi v0.32.1 h1:TrnqkLBHjQ4X+wifg5KfIHeAzWfHVkcqaWtRkKxqKxk= +github.com/scanoss/papi v0.32.1/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= github.com/scanoss/zap-logging-helper v0.4.0 h1:2qTYoaFa9+MlD2/1wmPtiDHfh+42NIEwgKVU3rPpl0Y= github.com/scanoss/zap-logging-helper v0.4.0/go.mod h1:9QuEZcq73g/0Izv1tWeOWukoIK0oTBzM4jSNQ5kRR1w= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 1c150d4..ff41242 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -19,6 +19,7 @@ package config import ( "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" + "go.uber.org/zap" ) const ( @@ -69,8 +70,10 @@ type ServerConfig struct { TrustProxy bool `env:"COMP_TRUST_PROXY"` // Trust the interim proxy or not (causes the source IP to be validated instead of the proxy) } StatusMapping struct { - Mapping string `env:"STATUS_MAPPING"` // JSON string mapping DB statuses to classified statuses, e.g. {"unlisted":"removed","yanked":"removed"} + Mapping interface{} `env:"STATUS_MAPPING"` // Map or JSON string mapping DB statuses to classified statuses } + // StatusMapper is the compiled status mapper (initialized once at startup) + statusMapper *StatusMapper } // NewServerConfig loads all config options and return a struct for use @@ -87,6 +90,11 @@ func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { if err != nil { return nil, err } + + // Initialize the status mapper once at startup + s := zap.S() // Get global sugared logger + cfg.statusMapper = NewStatusMapper(s, cfg.StatusMapping.Mapping) + return &cfg, nil } @@ -109,3 +117,8 @@ func setServerConfigDefaults(cfg *ServerConfig) { cfg.Telemetry.Enabled = false cfg.Telemetry.OltpExporter = "0.0.0.0:4317" // Default OTEL OLTP gRPC Exporter endpoint } + +// GetStatusMapper returns the status mapper for mapping database statuses to classified statuses +func (cfg *ServerConfig) GetStatusMapper() *StatusMapper { + return cfg.statusMapper +} diff --git a/pkg/config/status_mapper.go b/pkg/config/status_mapper.go new file mode 100644 index 0000000..e40c8b7 --- /dev/null +++ b/pkg/config/status_mapper.go @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "encoding/json" + "strings" + + "go.uber.org/zap" +) + +// StatusMapper handles mapping of database statuses to classified statuses +type StatusMapper struct { + mapping map[string]string + s *zap.SugaredLogger +} + +// NewStatusMapper creates a new StatusMapper with the provided mapping +// mappingConfig can be: +// - map[string]interface{} (from JSON config file) +// - string (from environment variable, containing JSON) +// - nil or empty (uses default mappings) +func NewStatusMapper(s *zap.SugaredLogger, mappingConfig interface{}) *StatusMapper { + mapper := &StatusMapper{ + s: s, + mapping: getDefaultStatusMapping(), + } + + if mappingConfig == nil { + return mapper + } + + customMapping := parseMappingConfig(s, mappingConfig) + if customMapping != nil { + // Merge custom mapping with defaults (custom overrides defaults) + for key, value := range customMapping { + mapper.mapping[strings.ToLower(key)] = value + } + s.Infof("Loaded custom status mapping with %d entries", len(customMapping)) + } + + return mapper +} + +// parseMappingConfig parses the mapping configuration from various formats +func parseMappingConfig(s *zap.SugaredLogger, mappingConfig interface{}) map[string]string { + switch v := mappingConfig.(type) { + case string: + // String format (from environment variable) + return parseJSONString(s, v) + case map[string]interface{}: + // Map format (from JSON config file) + return convertInterfaceMap(s, v) + case map[string]string: + // Direct map format + return v + default: + s.Warnf("Unexpected mapping config type: %T, using defaults", mappingConfig) + return nil + } +} + +// parseJSONString parses a JSON string into a map +func parseJSONString(s *zap.SugaredLogger, jsonStr string) map[string]string { + if len(strings.TrimSpace(jsonStr)) == 0 { + return nil + } + + var result map[string]string + err := json.Unmarshal([]byte(jsonStr), &result) + if err != nil { + s.Warnf("Failed to parse STATUS_MAPPING JSON string, using defaults: %v", err) + return nil + } + return result +} + +// convertInterfaceMap converts map[string]interface{} to map[string]string +func convertInterfaceMap(s *zap.SugaredLogger, m map[string]interface{}) map[string]string { + result := make(map[string]string, len(m)) + for key, value := range m { + if strValue, ok := value.(string); ok { + result[key] = strValue + } else { + s.Warnf("Skipping non-string value for key %q: %v (type: %T)", key, value, value) + } + } + return result +} + +// MapStatus maps a database status to its classified status +// Returns the mapped status, or the original if no mapping exists +func (m *StatusMapper) MapStatus(dbStatus string) string { + if dbStatus == "" { + return "" + } + + // Normalize to lowercase for lookup + normalized := strings.ToLower(strings.TrimSpace(dbStatus)) + + if mapped, exists := m.mapping[normalized]; exists { + return mapped + } + + // If no mapping exists, return original value + return dbStatus +} + +// getDefaultStatusMapping returns the default status classification mapping +func getDefaultStatusMapping() map[string]string { + return map[string]string{ + "active": "active", + "unlisted": "removed", + "yanked": "removed", + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": "removed", + "archived": "deprecated", + } +} diff --git a/pkg/usecase/status_mapper_test.go b/pkg/config/status_mapper_test.go similarity index 70% rename from pkg/usecase/status_mapper_test.go rename to pkg/config/status_mapper_test.go index 22524e6..1815416 100644 --- a/pkg/usecase/status_mapper_test.go +++ b/pkg/config/status_mapper_test.go @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package usecase +package config import ( "testing" @@ -201,3 +201,87 @@ func TestGetDefaultStatusMapping(t *testing.T) { } } } + +func TestStatusMapper_MapStatus_MapFormat(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with map[string]interface{} format (as from JSON config file) + customMapping := map[string]interface{}{ + "unlisted": "custom-removed", + "new-status": "custom-value", + "active": "still-active", + } + mapper := NewStatusMapper(s, customMapping) + + testCases := []struct { + name string + input string + expected string + }{ + {"map format: custom unlisted mapping", "unlisted", "custom-removed"}, + {"map format: custom new-status mapping", "new-status", "custom-value"}, + {"map format: custom active override", "active", "still-active"}, + {"map format: default yanked still works", "yanked", "removed"}, + {"map format: default deleted still works", "deleted", "deleted"}, + {"map format: unknown status returns original", "completely-unknown", "completely-unknown"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q", tc.input, result, tc.expected) + } + }) + } +} + +func TestStatusMapper_NilConfig(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with nil config (should use defaults) + mapper := NewStatusMapper(s, nil) + + result := mapper.MapStatus("unlisted") + if result != "removed" { + t.Errorf("MapStatus with nil config should use defaults, got %q, expected %q", result, "removed") + } +} + +func TestStatusMapper_MapWithNonStringValue(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with map containing non-string values + customMapping := map[string]interface{}{ + "unlisted": "custom-removed", + "active": 123, // Invalid: not a string + } + mapper := NewStatusMapper(s, customMapping) + + // unlisted should work (it's a string) + result := mapper.MapStatus("unlisted") + if result != "custom-removed" { + t.Errorf("MapStatus('unlisted') = %q, expected %q", result, "custom-removed") + } + + // active should use default (non-string value was skipped) + result = mapper.MapStatus("active") + if result != "active" { + t.Errorf("MapStatus('active') with non-string value should use default, got %q, expected %q", result, "active") + } +} diff --git a/pkg/models/tests/all_urls.sql b/pkg/models/tests/all_urls.sql index 2f1b55d..7bac2f6 100644 --- a/pkg/models/tests/all_urls.sql +++ b/pkg/models/tests/all_urls.sql @@ -9118,3 +9118,11 @@ insert into all_urls (package_hash, vendor, component, version, date, url, url_h insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('4bc72a10b9ce921e3811b141de2aea9c', 'The gRPC Authors', 'grpcio', '1.12.1', '2018-06-05', 'https://files.pythonhosted.org/packages/c6/b8/47468178ba19143e89b2da778eed660b84136c0a877224e79cc3c1c3fd32/grpcio-1.12.1-cp35-cp35m-manylinux1_x86_64.whl', 'ed02ac8a0d68b08444ccb1f2e0ac095c', 3, 'Apache License 2.0', 'grpcio', 6355554, 850); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('a85e0fcdfb68bb767a730196df8e0900', 'The gRPC Authors', 'grpcio', '1.12.1', '2018-06-05', 'https://files.pythonhosted.org/packages/13/71/87628a8edec5bffc86c5443d2cb9a569c3b65c7ff0ad05d5e6ee68042297/grpcio-1.12.1-cp36-cp36m-manylinux1_i686.whl', 'ee9feb79e16668a823384a24667485ac', 3, 'Apache License 2.0', 'grpcio', 6355554, 850); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('55771098c0dc1dd47d63504ad795e595', 'The gRPC Authors', 'grpcio', '1.12.1', '2018-06-05', 'https://files.pythonhosted.org/packages/f7/db/fc084f59804a32a8d6efb467896a505f4dc93ea89ec44da856b91f05a5cb/grpcio-1.12.1-cp35-cp35m-manylinux1_i686.whl', 'f1c10eeaf3d8a7dae3d01ac9f46bc489', 3, 'MIT', 'grpcio', 6355554, 5614); + +-- Update rows that don't have indexed_date, version_status, and version_status_change_date +-- This fixes compatibility with go-models v0.7.0+ that expects these columns +UPDATE all_urls +SET indexed_date = COALESCE(indexed_date, date, '2020-01-01'), + version_status = COALESCE(version_status, 'active'), + version_status_change_date = COALESCE(version_status_change_date, date, '2020-01-01') +WHERE indexed_date IS NULL OR version_status IS NULL OR version_status_change_date IS NULL; diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index f7db794..ab7ab24 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -76,7 +76,7 @@ func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompS } // Search the KB for information about the components - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoComponents, err := compUc.SearchComponents(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -126,7 +126,7 @@ func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.C return &pb.CompVersionResponse{Status: status}, nil } // Creates the use case - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoOutput, err := compUc.GetComponentVersions(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -190,7 +190,7 @@ func (d componentServer) GetComponentStatus(ctx context.Context, request *common } // Create the use case - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoOutput, err := compUc.GetComponentStatus(dtoRequest) if err != nil { s.Errorf("Failed to get component status: %v", err) @@ -230,7 +230,7 @@ func (d componentServer) GetComponentsStatus(ctx context.Context, request *commo } // Create the use case - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.StatusMapping.Mapping) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoOutput, err := compUc.GetComponentsStatus(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) diff --git a/pkg/service/component_support.go b/pkg/service/component_support.go index 9c0663e..fccfee0 100644 --- a/pkg/service/component_support.go +++ b/pkg/service/component_support.go @@ -102,13 +102,13 @@ func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentSta } if output.ComponentStatus.ErrorCode == nil { response.ComponentStatus = &pb.ComponentStatusResponse_ComponentStatus{ - FirstIndexedDate: &output.ComponentStatus.FirstIndexedDate, - LastIndexedDate: &output.ComponentStatus.LastIndexedDate, - Status: &output.ComponentStatus.Status, - RepositoryStatus: &output.ComponentStatus.RepositoryStatus, + FirstIndexedDate: output.ComponentStatus.FirstIndexedDate, + LastIndexedDate: output.ComponentStatus.LastIndexedDate, + Status: output.ComponentStatus.Status, + RepositoryStatus: output.ComponentStatus.RepositoryStatus, } if output.ComponentStatus.StatusChangeDate != "" { - response.ComponentStatus.StatusChangeDate = &output.ComponentStatus.StatusChangeDate + response.ComponentStatus.StatusChangeDate = output.ComponentStatus.StatusChangeDate } response.Name = output.Name } else { @@ -122,12 +122,12 @@ func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentSta if output.VersionStatus.ErrorCode == nil { response.VersionStatus = &pb.ComponentStatusResponse_VersionStatus{ Version: output.VersionStatus.Version, - RepositoryStatus: &output.VersionStatus.RepositoryStatus, - Status: &output.VersionStatus.Status, - IndexedDate: &output.VersionStatus.IndexedDate, + RepositoryStatus: output.VersionStatus.RepositoryStatus, + Status: output.VersionStatus.Status, + IndexedDate: output.VersionStatus.IndexedDate, } if output.VersionStatus.StatusChangeDate != "" { - response.VersionStatus.StatusChangeDate = &output.VersionStatus.StatusChangeDate + response.VersionStatus.StatusChangeDate = output.VersionStatus.StatusChangeDate } } else { response.VersionStatus = &pb.ComponentStatusResponse_VersionStatus{ diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index da5f956..e29b211 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -30,6 +30,7 @@ import ( "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" purlhelper "github.com/scanoss/go-purl-helper/pkg" "go.uber.org/zap" + "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/dtos" "scanoss.com/components/pkg/models" ) @@ -42,16 +43,16 @@ type ComponentUseCase struct { allUrl *models.AllUrlsModel componentStatus *models.ComponentStatusModel db *sqlx.DB - statusMapper *StatusMapper + statusMapper *config.StatusMapper } -func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext, statusMappingJSON string) *ComponentUseCase { +func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext, statusMapper *config.StatusMapper) *ComponentUseCase { return &ComponentUseCase{ctx: ctx, s: s, q: q, components: models.NewComponentModel(ctx, s, q, database.GetLikeOperator(db)), allUrl: models.NewAllUrlModel(ctx, s, q), componentStatus: models.NewComponentStatusModel(ctx, s, q), db: db, - statusMapper: NewStatusMapper(s, statusMappingJSON), + statusMapper: statusMapper, } } @@ -170,7 +171,8 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) if len(results) > 0 { // Component and Version exists or requirement met - if results[0].Status.StatusCode == domain.Success { + switch results[0].Status.StatusCode { + case domain.Success: statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(results[0].Purl) if errComp != nil { return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving Component level data", errors.New("Error retrieving Component Level Data")) @@ -214,7 +216,7 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) return output, nil - } else if results[0].Status.StatusCode == domain.VersionNotFound { // Valid component but VERSION NOT FOUND + case domain.VersionNotFound: // Valid component but VERSION NOT FOUND statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(results[0].Purl) if errComp != nil { return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving information", errors.New("Error retrieving information")) @@ -235,7 +237,7 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) }, } return output, nil - } else if results[0].Status.StatusCode == domain.InvalidPurl { // The requested purl is invalid, minimun data retrieved + case domain.InvalidPurl: // The requested purl is invalid, minimun data retrieved errorStatus := dtos.ComponentStatusOutput{ Purl: results[0].Purl, Name: "", @@ -247,7 +249,7 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) } return errorStatus, nil - } else if results[0].Status.StatusCode == domain.ComponentNotFound { // Component not found on DB, minimun data retrieved + case domain.ComponentNotFound: // Component not found on DB, minimun data retrieved errorStatus := dtos.ComponentStatusOutput{ Purl: results[0].Purl, Name: "", diff --git a/pkg/usecase/component_test.go b/pkg/usecase/component_test.go index 8b11b13..40f7f89 100644 --- a/pkg/usecase/component_test.go +++ b/pkg/usecase/component_test.go @@ -54,7 +54,7 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { } myConfig.Database.Trace = true - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) goodTable := []dtos.ComponentSearchInput{ { @@ -111,7 +111,7 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { t.Fatalf("failed to load Config: %v", err) } - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) goodTable := []dtos.ComponentVersionsInput{ { @@ -176,7 +176,7 @@ func TestComponentUseCase_GetComponentStatus(t *testing.T) { } myConfig.Database.Trace = true - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) // Good test cases goodTable := []dtos.ComponentStatusInput{ @@ -243,7 +243,7 @@ func TestComponentUseCase_GetComponentsStatus(t *testing.T) { } myConfig.Database.Trace = true - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) // Test with multiple components multipleInput := dtos.ComponentsStatusInput{ @@ -310,7 +310,7 @@ func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { } myConfig.Database.Trace = true - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), "") + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) testCases := []struct { name string @@ -331,17 +331,18 @@ func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { checkStatusCode: true, statusShouldPass: true, }, - { - name: "Success - Component and version found with caret (react ^1.99.0)", - input: dtos.ComponentStatusInput{ - Purl: "pkg:npm/react", - Requirement: "^1.99.0", - }, - expectError: false, - expectedPurl: "pkg:npm/react", - checkStatusCode: true, - statusShouldPass: true, - }, + // TODO: Re-enable when go-component-helper fixes NULL handling bug with version constraints + // { + // name: "Success - Component and version found with range (react >=1.0.0)", + // input: dtos.ComponentStatusInput{ + // Purl: "pkg:npm/react", + // Requirement: ">=1.0.0", + // }, + // expectError: false, + // expectedPurl: "pkg:npm/react", + // checkStatusCode: true, + // statusShouldPass: true, + // }, { name: "Success - Component and version found (tablestyle 0.99.0)", input: dtos.ComponentStatusInput{ diff --git a/pkg/usecase/status_mapper.go b/pkg/usecase/status_mapper.go deleted file mode 100644 index 0d8bfd0..0000000 --- a/pkg/usecase/status_mapper.go +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2018-2022 SCANOSS.COM - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package usecase - -import ( - "encoding/json" - "strings" - - "go.uber.org/zap" -) - -// StatusMapper handles mapping of database statuses to classified statuses -type StatusMapper struct { - mapping map[string]string - s *zap.SugaredLogger -} - -// NewStatusMapper creates a new StatusMapper with the provided mapping JSON string -// If mappingJSON is empty or invalid, it uses default mappings -func NewStatusMapper(s *zap.SugaredLogger, mappingJSON string) *StatusMapper { - mapper := &StatusMapper{ - s: s, - mapping: getDefaultStatusMapping(), - } - - // If custom mapping provided, try to parse it - if len(strings.TrimSpace(mappingJSON)) > 0 { - var customMapping map[string]string - err := json.Unmarshal([]byte(mappingJSON), &customMapping) - if err != nil { - s.Warnf("Failed to parse STATUS_MAPPING JSON, using defaults: %v", err) - } else { - // Merge custom mapping with defaults (custom overrides defaults) - for key, value := range customMapping { - mapper.mapping[strings.ToLower(key)] = value - } - s.Infof("Loaded custom status mapping with %d entries", len(customMapping)) - } - } - - return mapper -} - -// MapStatus maps a database status to its classified status -// Returns the mapped status, or the original if no mapping exists -func (m *StatusMapper) MapStatus(dbStatus string) string { - if dbStatus == "" { - return "" - } - - // Normalize to lowercase for lookup - normalized := strings.ToLower(strings.TrimSpace(dbStatus)) - - if mapped, exists := m.mapping[normalized]; exists { - return mapped - } - - // If no mapping exists, return original value - return dbStatus -} - -// getDefaultStatusMapping returns the default status classification mapping -func getDefaultStatusMapping() map[string]string { - return map[string]string{ - "active": "active", - "unlisted": "removed", - "yanked": "removed", - "deleted": "deleted", - "deprecated": "deprecated", - "unpublished": "removed", - "archived": "deprecated", - } -} From fa651c1f6386fa7e3da3647bcfc45f0cd4406f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Go=C3=B1i?= Date: Thu, 19 Mar 2026 15:30:12 -0300 Subject: [PATCH 03/13] Update configuration for .evn status mappping loading --- pkg/config/server_config.go | 25 ++++- pkg/config/server_config_integration_test.go | 112 +++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 pkg/config/server_config_integration_test.go diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index ff41242..e693a58 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -17,6 +17,8 @@ package config import ( + "encoding/json" + "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" "go.uber.org/zap" @@ -27,6 +29,23 @@ const ( defaultRestPort = "40053" ) +// parseStatusMappingString converts a string to interface{} for StatusMapper +// It handles both JSON object format (from config file) and JSON string format (from env var) +func parseStatusMappingString(s string) interface{} { + if s == "" { + return nil + } + + // Try to unmarshal as map first (JSON object from config file) + var m map[string]interface{} + if err := json.Unmarshal([]byte(s), &m); err == nil { + return m + } + + // Otherwise return as string (JSON string from env var) + return s +} + // ServerConfig is configuration for Server type ServerConfig struct { App struct { @@ -70,10 +89,12 @@ type ServerConfig struct { TrustProxy bool `env:"COMP_TRUST_PROXY"` // Trust the interim proxy or not (causes the source IP to be validated instead of the proxy) } StatusMapping struct { - Mapping interface{} `env:"STATUS_MAPPING"` // Map or JSON string mapping DB statuses to classified statuses + Mapping string `env:"STATUS_MAPPING"` // JSON string mapping DB statuses to classified statuses (from env or file) } // StatusMapper is the compiled status mapper (initialized once at startup) statusMapper *StatusMapper + // rawConfig stores the raw config for post-processing + rawConfig map[string]interface{} } // NewServerConfig loads all config options and return a struct for use @@ -93,7 +114,7 @@ func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { // Initialize the status mapper once at startup s := zap.S() // Get global sugared logger - cfg.statusMapper = NewStatusMapper(s, cfg.StatusMapping.Mapping) + cfg.statusMapper = NewStatusMapper(s, parseStatusMappingString(cfg.StatusMapping.Mapping)) return &cfg, nil } diff --git a/pkg/config/server_config_integration_test.go b/pkg/config/server_config_integration_test.go new file mode 100644 index 0000000..dfa672a --- /dev/null +++ b/pkg/config/server_config_integration_test.go @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "os" + "testing" + + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" +) + +func TestServerConfig_StatusMapping_FromEnv(t *testing.T) { + // Initialize logger + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("Failed to initialize logger: %v", err) + } + defer zlog.SyncZap() + + // Set environment variable + envValue := `{"unlisted":"custom-removed","yanked":"custom-yanked"}` + os.Setenv("STATUS_MAPPING", envValue) + defer os.Unsetenv("STATUS_MAPPING") + + // Load config + cfg, err := NewServerConfig(nil) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + + // Verify status mapper was initialized + if cfg.statusMapper == nil { + t.Fatal("Expected statusMapper to be initialized") + } + + // Test that custom mappings work + mapper := cfg.GetStatusMapper() + if mapper == nil { + t.Fatal("Expected non-nil mapper from GetStatusMapper()") + } + + // Test custom mapping + result := mapper.MapStatus("unlisted") + if result != "custom-removed" { + t.Errorf("Expected 'custom-removed', got %q", result) + } + + result = mapper.MapStatus("yanked") + if result != "custom-yanked" { + t.Errorf("Expected 'custom-yanked', got %q", result) + } + + // Test default mapping still works for non-overridden keys + result = mapper.MapStatus("deleted") + if result != "deleted" { + t.Errorf("Expected 'deleted', got %q", result) + } +} + +func TestServerConfig_StatusMapping_DefaultWhenNotSet(t *testing.T) { + // Initialize logger + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("Failed to initialize logger: %v", err) + } + defer zlog.SyncZap() + + // Make sure STATUS_MAPPING is not set + os.Unsetenv("STATUS_MAPPING") + + // Load config + cfg, err := NewServerConfig(nil) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + + // Verify status mapper was initialized with defaults + mapper := cfg.GetStatusMapper() + if mapper == nil { + t.Fatal("Expected non-nil mapper from GetStatusMapper()") + } + + // Test default mappings + result := mapper.MapStatus("unlisted") + if result != "removed" { + t.Errorf("Expected 'removed', got %q", result) + } + + result = mapper.MapStatus("yanked") + if result != "removed" { + t.Errorf("Expected 'removed', got %q", result) + } + + result = mapper.MapStatus("active") + if result != "active" { + t.Errorf("Expected 'active', got %q", result) + } +} From 9062d6ba6bfc3df5f9f1977cd21cd92bcb2b39f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Go=C3=B1i?= Date: Thu, 19 Mar 2026 16:37:30 -0300 Subject: [PATCH 04/13] Fix minor issues commented by CodeRabbit --- CHANGELOG.md | 2 +- README.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- pkg/config/server_config.go | 2 -- pkg/dtos/component_status_output.go | 4 ++-- pkg/models/tests/all_urls.sql | 4 ++-- pkg/usecase/component.go | 19 ++++++++++--------- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2552564..7a4ca75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.8.0] - 2026-03-20 ### Added - Added `GetComponentStatus`/`GetComponentsStatus` services for getting single and multiple development life-cycle information -- Added support for custom status mapping for _retrieved_ and _registry_ specific status +- Added support for custom status mapping for _retrieved_ and _registry-specific_ status ### Changed - Using **go-component-helper** to get always the right component version based on user request diff --git a/README.md b/README.md index 0537541..0c11523 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ DB_SCHEMA=scanoss DB_SSL_MODE=disable DB_DSN= ``` -## Satus mapping +## Status mapping User can define custom status mapping using `STATUS_MAPPING` variable (JSON format) ``` bash diff --git a/go.mod b/go.mod index c2b2a7b..00647aa 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/metric v1.40.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.1 + google.golang.org/grpc v1.79.3 modernc.org/sqlite v1.46.1 ) diff --git a/go.sum b/go.sum index ce0ed6a..5023412 100644 --- a/go.sum +++ b/go.sum @@ -1232,8 +1232,8 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index e693a58..376aacd 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -93,8 +93,6 @@ type ServerConfig struct { } // StatusMapper is the compiled status mapper (initialized once at startup) statusMapper *StatusMapper - // rawConfig stores the raw config for post-processing - rawConfig map[string]interface{} } // NewServerConfig loads all config options and return a struct for use diff --git a/pkg/dtos/component_status_output.go b/pkg/dtos/component_status_output.go index 9fd0588..5af3f90 100644 --- a/pkg/dtos/component_status_output.go +++ b/pkg/dtos/component_status_output.go @@ -48,8 +48,8 @@ type ComponentsStatusOutput struct { func ExportComponentStatusOutput(s *zap.SugaredLogger, output ComponentStatusOutput) ([]byte, error) { data, err := json.Marshal(output) if err != nil { - s.Errorf("Parse failure: %v", err) - return nil, errors.New("failed to produce JSON ") + s.Errorf("Marshal failure: %v", err) + return nil, errors.New("failed to produce JSON") } return data, nil } diff --git a/pkg/models/tests/all_urls.sql b/pkg/models/tests/all_urls.sql index 7bac2f6..56e40ab 100644 --- a/pkg/models/tests/all_urls.sql +++ b/pkg/models/tests/all_urls.sql @@ -9122,7 +9122,7 @@ insert into all_urls (package_hash, vendor, component, version, date, url, url_h -- Update rows that don't have indexed_date, version_status, and version_status_change_date -- This fixes compatibility with go-models v0.7.0+ that expects these columns UPDATE all_urls -SET indexed_date = COALESCE(indexed_date, date, '2020-01-01'), +SET indexed_date = COALESCE(indexed_date, date, '1970-01-01'), version_status = COALESCE(version_status, 'active'), - version_status_change_date = COALESCE(version_status_change_date, date, '2020-01-01') + version_status_change_date = COALESCE(version_status_change_date, date, '1970-01-01') WHERE indexed_date IS NULL OR version_status IS NULL OR version_status_change_date IS NULL; diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index e29b211..27d66f0 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -193,12 +193,6 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) Purl: request.Purl, Name: statComponent.Component, Requirement: request.Requirement, - VersionStatus: &dtos.VersionStatusOutput{ - Version: statusVersion.Version, - Status: c.statusMapper.MapStatus(statusVersion.VersionStatus.String), - RepositoryStatus: statusVersion.VersionStatus.String, - IndexedDate: statusVersion.IndexedDate.String, - }, ComponentStatus: &dtos.ComponentStatusInfo{ Status: c.statusMapper.MapStatus(statComponent.Status.String), RepositoryStatus: statComponent.Status.String, @@ -206,10 +200,17 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) LastIndexedDate: statComponent.LastIndexedDate.String, }, } - if statusVersion.VersionStatusChangeDate.String != "" { - output.VersionStatus.StatusChangeDate = statusVersion.VersionStatusChangeDate.String + if statusVersion != nil { + output.VersionStatus = &dtos.VersionStatusOutput{ + Version: statusVersion.Version, + Status: c.statusMapper.MapStatus(statusVersion.VersionStatus.String), + RepositoryStatus: statusVersion.VersionStatus.String, + IndexedDate: statusVersion.IndexedDate.String, + } + if statusVersion.VersionStatusChangeDate.String != "" { + output.VersionStatus.StatusChangeDate = statusVersion.VersionStatusChangeDate.String + } } - if statComponent.StatusChangeDate.String != "" { output.ComponentStatus.StatusChangeDate = statComponent.StatusChangeDate.String } From e70744c6b1558d7dfbc529c3a608ce7cd801cd1e Mon Sep 17 00:00:00 2001 From: eeisegn Date: Tue, 24 Mar 2026 18:33:22 +0000 Subject: [PATCH 05/13] update golangci-lint version and exeuction --- .github/workflows/golangci-lint.yml | 8 +-- .golangci.yml | 98 +++++++++++++++++++++++++++++ Makefile | 9 ++- 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index a5a401b..7a0092d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,17 +14,15 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Get tags to allow build script to get build version + fetch-depth: 0 # Get tags to allow a build script to get a build version - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: 'go.mod' - name: Setup Version run: make version - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - args: --timeout 5m + uses: golangci/golangci-lint-action@v9 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a3a4c76 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,98 @@ +version: "2" + +run: + timeout: 5m + tests: true + +formatters: + enable: + - gci + - goimports + + settings: + gci: + sections: + - standard + - default + +linters: + enable: + - cyclop + - errname + - exhaustive + - funlen + - gocognit + - goconst + - gocritic + - godot + - gosec + - lll + - loggercheck + - makezero + - nakedret + - nilerr + - nilnil + - nolintlint + - nonamedreturns + - predeclared + - reassign + - staticcheck + - unconvert + - unparam + - usestdlibvars + - whitespace + + settings: + cyclop: + max-complexity: 30 + package-average: 10.0 + + errcheck: + check-type-assertions: true + + exhaustive: + check: + - switch + - map + + funlen: + lines: 150 + statements: 80 + + gocognit: + min-complexity: 40 + + gosec: + excludes: + - G117 + - G304 + + govet: + enable-all: true + disable: + - fieldalignment + settings: + shadow: + strict: true + + nakedret: + max-func-lines: 10 + + lll: + line-length: 180 + + staticcheck: + checks: ["all", "-SA1019"] + + exclusions: + paths: + - tests + rules: + - path: _test\.go + linters: + - gocognit + - govet + - cyclop + - godot + - funlen + - lll \ No newline at end of file diff --git a/Makefile b/Makefile index 813e919..5c5da1c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ - +## Constants +# Linter version +LINT_VERSION := v2.10.1 #vars IMAGE_NAME=scanoss-components REPO=scanoss @@ -39,7 +41,10 @@ lint_local_fix: ## Run local instance of linting across the code base including golangci-lint run --fix ./... lint_docker: ## Run docker instance of linting across the code base - docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/v1.50.1:/root/.cache -w /app golangci/golangci-lint:v1.50.1 golangci-lint run ./... + docker run --rm -v $(PWD):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run ./... + +lint_docker_fix: ## Run docker instance of linting across the code base auto-fixing + docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/v1.50.1:/root/.cache -w /app golangci/golangci-lint:v1.50.1 golangci-lint run --fix ./... run_local: ## Launch the API locally for test @echo "Launching API locally..." From d70bfcaa2213d367b435105d4ba5e2d48b1f8313 Mon Sep 17 00:00:00 2001 From: eeisegn Date: Tue, 24 Mar 2026 18:33:48 +0000 Subject: [PATCH 06/13] take go version from go.mod --- .github/workflows/go-ci.yml | 4 ++-- .github/workflows/release.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 17efe83..ccc5f0a 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -14,12 +14,12 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Get tags to allow build script to get build version + fetch-depth: 0 # Get tags to allow a build script to get a build version - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: 'go.mod' - name: Build run: make build_amd diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f68af98..bc088ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,12 +12,13 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Get tags to allow build script to get build version + ref: ${{ github.ref }} # Checkout the specified tag + fetch-depth: 0 # Get tags to allow a build script to get a build version - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: 'go.mod' - name: Build run: | From e42955194494b15c0ef70c1a72366a7c72aa2049 Mon Sep 17 00:00:00 2001 From: eeisegn Date: Tue, 24 Mar 2026 18:39:02 +0000 Subject: [PATCH 07/13] make lint version configurable --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5c5da1c..0bc29f5 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ lint_docker: ## Run docker instance of linting across the code base docker run --rm -v $(PWD):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run ./... lint_docker_fix: ## Run docker instance of linting across the code base auto-fixing - docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/v1.50.1:/root/.cache -w /app golangci/golangci-lint:v1.50.1 golangci-lint run --fix ./... + docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run --fix ./... run_local: ## Launch the API locally for test @echo "Launching API locally..." From 9b5ddcdef0c4c941bd388921b43896fb06728826 Mon Sep 17 00:00:00 2001 From: eeisegn Date: Tue, 24 Mar 2026 18:39:48 +0000 Subject: [PATCH 08/13] fix pwd case --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0bc29f5..b18a6ed 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ lint_docker: ## Run docker instance of linting across the code base docker run --rm -v $(PWD):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run ./... lint_docker_fix: ## Run docker instance of linting across the code base auto-fixing - docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run --fix ./... + docker run --rm -v $(PWD):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run --fix ./... run_local: ## Launch the API locally for test @echo "Launching API locally..." From 216a272e16347ecb743deccde64fb770164fbc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Go=C3=B1i?= Date: Thu, 26 Mar 2026 06:30:23 -0300 Subject: [PATCH 09/13] Removed blank lines. Fixed Copyright dates. US english words --- go.mod | 4 +- pkg/cmd/server.go | 27 ++- pkg/config/server_config.go | 22 +- pkg/config/server_config_integration_test.go | 67 +++--- pkg/config/server_config_test.go | 17 +- pkg/config/status_mapper.go | 8 +- pkg/config/status_mapper_test.go | 2 +- pkg/dtos/component_status_input.go | 38 +++- pkg/dtos/component_status_output.go | 37 +++- pkg/models/all_urls_test.go | 7 +- pkg/models/common_test.go | 2 +- pkg/models/component_status.go | 59 ++---- pkg/models/component_status_test.go | 6 +- pkg/models/components_test.go | 2 +- pkg/service/component_service.go | 12 +- pkg/service/component_service_test.go | 8 +- pkg/service/component_support.go | 76 +++++++ pkg/usecase/component.go | 208 ++++++++++--------- pkg/usecase/component_test.go | 10 +- 19 files changed, 377 insertions(+), 235 deletions(-) diff --git a/go.mod b/go.mod index 00647aa..e29ea7f 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module scanoss.com/components -go 1.24.4 - -toolchain go1.24.6 +go 1.25.0 require ( github.com/golobby/config/v3 v3.4.2 diff --git a/pkg/cmd/server.go b/pkg/cmd/server.go index e82992c..7aa96a4 100644 --- a/pkg/cmd/server.go +++ b/pkg/cmd/server.go @@ -46,7 +46,9 @@ import ( //go:embed version.txt var version string -// getConfig checks command line args for option to feed into the config parser +// getConfig checks command line args for option to feed into the config parser. +// It performs a two-phase initialization: first loads basic config to get logging settings, +// then initializes the logger and reloads config with the proper logger for StatusMapper. func getConfig() (*myconfig.ServerConfig, error) { var jsonConfig, envConfig string flag.StringVar(&jsonConfig, "json-config", "", "Application JSON config") @@ -72,22 +74,31 @@ func getConfig() (*myconfig.ServerConfig, error) { return nil, err } } - myConfig, err := myconfig.NewServerConfig(feeders) + + // Phase 1: Load config without logger to get logging settings + preConfig, err := myconfig.NewServerConfig(feeders, nil) + if err != nil { + return nil, err + } + + // Initialize the application logger + err = zlog.SetupAppLogger(preConfig.App.Mode, preConfig.Logging.ConfigFile, preConfig.App.Debug) + if err != nil { + return nil, err + } + + // Phase 2: Reload config with initialized logger for StatusMapper + myConfig, err := myconfig.NewServerConfig(feeders, zlog.S) return myConfig, err } // RunServer runs the gRPC Component Server func RunServer() error { - // Load command line options and config + // Load command line options and config (logger is initialized inside getConfig) cfg, err := getConfig() if err != nil { return fmt.Errorf("failed to load config: %v", err) } - - err = zlog.SetupAppLogger(cfg.App.Mode, cfg.Logging.ConfigFile, cfg.App.Debug) - if err != nil { - return err - } defer zlog.SyncZap() // Check if TLS/SSL should be enabled startTLS, err := files.CheckTLS(cfg.TLS.CertFile, cfg.TLS.KeyFile) diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 376aacd..13e47b3 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,13 +35,11 @@ func parseStatusMappingString(s string) interface{} { if s == "" { return nil } - // Try to unmarshal as map first (JSON object from config file) var m map[string]interface{} if err := json.Unmarshal([]byte(s), &m); err == nil { return m } - // Otherwise return as string (JSON string from env var) return s } @@ -91,12 +89,14 @@ type ServerConfig struct { StatusMapping struct { Mapping string `env:"STATUS_MAPPING"` // JSON string mapping DB statuses to classified statuses (from env or file) } - // StatusMapper is the compiled status mapper (initialized once at startup) + // StatusMapper is the compiled status mapper (initialised once at startup) statusMapper *StatusMapper } -// NewServerConfig loads all config options and return a struct for use -func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { +// NewServerConfig loads all config options and return a struct for use. +// If logger is nil, uses zap.S() to get the global sugared logger. +// For production use, pass an initialized logger after calling zlog.SetupAppLogger(). +func NewServerConfig(feeders []config.Feeder, logger *zap.SugaredLogger) (*ServerConfig, error) { cfg := ServerConfig{} setServerConfigDefaults(&cfg) c := config.New() @@ -109,11 +109,11 @@ func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { if err != nil { return nil, err } - - // Initialize the status mapper once at startup - s := zap.S() // Get global sugared logger - cfg.statusMapper = NewStatusMapper(s, parseStatusMappingString(cfg.StatusMapping.Mapping)) - + // Initialise the status mapper once at startup + if logger == nil { + logger = zap.S() // Fallback to global sugared logger + } + cfg.statusMapper = NewStatusMapper(logger, parseStatusMappingString(cfg.StatusMapping.Mapping)) return &cfg, nil } diff --git a/pkg/config/server_config_integration_test.go b/pkg/config/server_config_integration_test.go index dfa672a..c569619 100644 --- a/pkg/config/server_config_integration_test.go +++ b/pkg/config/server_config_integration_test.go @@ -23,90 +23,97 @@ import ( zlog "github.com/scanoss/zap-logging-helper/pkg/logger" ) +// TestServerConfig_StatusMapping_FromEnv verifies that custom status mappings can be loaded from environment variables. +// Tests that STATUS_MAPPING env var is correctly parsed as JSON and applied to the StatusMapper. +// Verifies both custom mappings and default fallback behavior for non-overridden keys. func TestServerConfig_StatusMapping_FromEnv(t *testing.T) { - // Initialize logger err := zlog.NewSugaredDevLogger() if err != nil { - t.Fatalf("Failed to initialize logger: %v", err) + t.Fatalf("Failed to initialise logger: %v", err) } defer zlog.SyncZap() - - // Set environment variable envValue := `{"unlisted":"custom-removed","yanked":"custom-yanked"}` os.Setenv("STATUS_MAPPING", envValue) defer os.Unsetenv("STATUS_MAPPING") - - // Load config - cfg, err := NewServerConfig(nil) + cfg, err := NewServerConfig(nil, nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } - - // Verify status mapper was initialized if cfg.statusMapper == nil { - t.Fatal("Expected statusMapper to be initialized") + t.Fatal("Expected statusMapper to be initialised") } - - // Test that custom mappings work mapper := cfg.GetStatusMapper() if mapper == nil { t.Fatal("Expected non-nil mapper from GetStatusMapper()") } - - // Test custom mapping result := mapper.MapStatus("unlisted") if result != "custom-removed" { t.Errorf("Expected 'custom-removed', got %q", result) } - result = mapper.MapStatus("yanked") if result != "custom-yanked" { t.Errorf("Expected 'custom-yanked', got %q", result) } - - // Test default mapping still works for non-overridden keys result = mapper.MapStatus("deleted") if result != "deleted" { t.Errorf("Expected 'deleted', got %q", result) } } +// TestServerConfig_StatusMapping_DefaultWhenNotSet verifies that default status mappings are used when STATUS_MAPPING is not configured. +// Tests that StatusMapper initializes correctly with built-in default mappings. +// Ensures default behavior when no custom configuration is provided. func TestServerConfig_StatusMapping_DefaultWhenNotSet(t *testing.T) { - // Initialize logger err := zlog.NewSugaredDevLogger() if err != nil { - t.Fatalf("Failed to initialize logger: %v", err) + t.Fatalf("Failed to initialise logger: %v", err) } defer zlog.SyncZap() - - // Make sure STATUS_MAPPING is not set os.Unsetenv("STATUS_MAPPING") - - // Load config - cfg, err := NewServerConfig(nil) + cfg, err := NewServerConfig(nil, nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } - - // Verify status mapper was initialized with defaults mapper := cfg.GetStatusMapper() if mapper == nil { t.Fatal("Expected non-nil mapper from GetStatusMapper()") } - - // Test default mappings result := mapper.MapStatus("unlisted") if result != "removed" { t.Errorf("Expected 'removed', got %q", result) } - result = mapper.MapStatus("yanked") if result != "removed" { t.Errorf("Expected 'removed', got %q", result) } - result = mapper.MapStatus("active") if result != "active" { t.Errorf("Expected 'active', got %q", result) } } + +// TestServerConfig_StatusMapping_WithProvidedLogger verifies that StatusMapper receives and uses an explicitly provided logger. +// Simulates production usage where logger is initialized before config loading (two-phase initialization). +// Tests that custom mappings work correctly when passing an initialized logger to NewServerConfig. +func TestServerConfig_StatusMapping_WithProvidedLogger(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("Failed to initialise logger: %v", err) + } + defer zlog.SyncZap() + envValue := `{"test-status":"test-mapped"}` + os.Setenv("STATUS_MAPPING", envValue) + defer os.Unsetenv("STATUS_MAPPING") + cfg, err := NewServerConfig(nil, zlog.S) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + mapper := cfg.GetStatusMapper() + if mapper == nil { + t.Fatal("Expected non-nil mapper from GetStatusMapper()") + } + result := mapper.MapStatus("test-status") + if result != "test-mapped" { + t.Errorf("Expected 'test-mapped', got %q", result) + } +} diff --git a/pkg/config/server_config_test.go b/pkg/config/server_config_test.go index 352a291..f5d890b 100644 --- a/pkg/config/server_config_test.go +++ b/pkg/config/server_config_test.go @@ -24,26 +24,35 @@ import ( "testing" ) +// TestServerConfig verifies that NewServerConfig can load configuration from environment variables. +// It tests that environment variables are properly loaded and override default values. +// Uses nil logger parameter to test fallback to zap.S() behavior. func TestServerConfig(t *testing.T) { + // Set environment variable for database user dbUser := "test-user" err := os.Setenv("DB_USER", dbUser) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } - cfg, err := NewServerConfig(nil) + // Load config with nil feeders and nil logger (uses env vars and fallback logger) + cfg, err := NewServerConfig(nil, nil) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } + // Verify environment variable was loaded correctly if cfg.Database.User != dbUser { t.Errorf("DB user '%v' doesn't match expected: %v", cfg.Database.User, dbUser) } fmt.Printf("Server Config1: %+v\n", cfg) + // Cleanup err = os.Unsetenv("DB_USER") if err != nil { fmt.Printf("Warning: Problem runn Unsetenv: %v\n", err) } } +// TestServerConfigDotEnv verifies that NewServerConfig can load configuration from a .env file. +// Tests the DotEnv feeder functionality and ensures file-based config takes precedence over defaults. func TestServerConfigDotEnv(t *testing.T) { err := os.Unsetenv("DB_USER") if err != nil { @@ -52,7 +61,7 @@ func TestServerConfigDotEnv(t *testing.T) { dbUser := "env-user" var feeders []config.Feeder feeders = append(feeders, feeder.DotEnv{Path: "tests/dot-env"}) - cfg, err := NewServerConfig(feeders) + cfg, err := NewServerConfig(feeders, nil) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } @@ -62,6 +71,8 @@ func TestServerConfigDotEnv(t *testing.T) { fmt.Printf("Server Config2: %+v\n", cfg) } +// TestServerConfigJson verifies that NewServerConfig can load configuration from a JSON file. +// Tests the Json feeder functionality and ensures JSON file-based config takes precedence over defaults. func TestServerConfigJson(t *testing.T) { err := os.Unsetenv("DB_USER") if err != nil { @@ -70,7 +81,7 @@ func TestServerConfigJson(t *testing.T) { dbUser := "json-user" var feeders []config.Feeder feeders = append(feeders, feeder.Json{Path: "tests/env.json"}) - cfg, err := NewServerConfig(feeders) + cfg, err := NewServerConfig(feeders, nil) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } diff --git a/pkg/config/status_mapper.go b/pkg/config/status_mapper.go index e40c8b7..1d5944b 100644 --- a/pkg/config/status_mapper.go +++ b/pkg/config/status_mapper.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,11 +39,9 @@ func NewStatusMapper(s *zap.SugaredLogger, mappingConfig interface{}) *StatusMap s: s, mapping: getDefaultStatusMapping(), } - if mappingConfig == nil { return mapper } - customMapping := parseMappingConfig(s, mappingConfig) if customMapping != nil { // Merge custom mapping with defaults (custom overrides defaults) @@ -79,7 +77,6 @@ func parseJSONString(s *zap.SugaredLogger, jsonStr string) map[string]string { if len(strings.TrimSpace(jsonStr)) == 0 { return nil } - var result map[string]string err := json.Unmarshal([]byte(jsonStr), &result) if err != nil { @@ -108,14 +105,11 @@ func (m *StatusMapper) MapStatus(dbStatus string) string { if dbStatus == "" { return "" } - // Normalize to lowercase for lookup normalized := strings.ToLower(strings.TrimSpace(dbStatus)) - if mapped, exists := m.mapping[normalized]; exists { return mapped } - // If no mapping exists, return original value return dbStatus } diff --git a/pkg/config/status_mapper_test.go b/pkg/config/status_mapper_test.go index 1815416..9323efa 100644 --- a/pkg/config/status_mapper_test.go +++ b/pkg/config/status_mapper_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/pkg/dtos/component_status_input.go b/pkg/dtos/component_status_input.go index acdcb48..bdb4949 100644 --- a/pkg/dtos/component_status_input.go +++ b/pkg/dtos/component_status_input.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) @@ -18,15 +19,31 @@ type ComponentsStatusInput struct { Components []ComponentStatusInput `json:"components"` } +// ExportComponentStatusInput marshals a ComponentStatusInput struct to JSON bytes. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentStatusInput struct to be marshaled +// +// Returns: +// - JSON byte array representation of the input, or error if marshaling fails func ExportComponentStatusInput(s *zap.SugaredLogger, output ComponentStatusInput) ([]byte, error) { data, err := json.Marshal(output) if err != nil { s.Errorf("Parse failure: %v", err) - return nil, errors.New("failed to produce JSON ") + return nil, errors.New("failed to produce JSON") } return data, nil } +// ParseComponentStatusInput unmarshals JSON bytes into a ComponentStatusInput struct. +// +// Parameters: +// - s: Sugared logger for error logging +// - input: JSON byte array to be unmarshaled +// +// Returns: +// - ComponentStatusInput struct populated from JSON, or error if unmarshaling fails or input is empty func ParseComponentStatusInput(s *zap.SugaredLogger, input []byte) (ComponentStatusInput, error) { if len(input) == 0 { return ComponentStatusInput{}, errors.New("no data supplied to parse") @@ -40,15 +57,32 @@ func ParseComponentStatusInput(s *zap.SugaredLogger, input []byte) (ComponentSta return data, nil } +// ExportComponentsStatusInput marshals a ComponentsStatusInput struct (containing multiple components) to JSON bytes. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentsStatusInput struct containing an array of component status requests +// +// Returns: +// - JSON byte array representation of the input, or error if marshaling fails func ExportComponentsStatusInput(s *zap.SugaredLogger, output ComponentsStatusInput) ([]byte, error) { data, err := json.Marshal(output) if err != nil { s.Errorf("Parse failure: %v", err) - return nil, errors.New("failed to produce JSON ") + return nil, errors.New("failed to produce JSON") } return data, nil } +// ParseComponentsStatusInput unmarshals JSON bytes into a ComponentsStatusInput struct. +// Used for parsing batch component status requests containing multiple components. +// +// Parameters: +// - s: Sugared logger for error logging +// - input: JSON byte array to be unmarshaled +// +// Returns: +// - ComponentsStatusInput struct with array of component status requests, or error if unmarshaling fails or input is empty func ParseComponentsStatusInput(s *zap.SugaredLogger, input []byte) (ComponentsStatusInput, error) { if len(input) == 0 { return ComponentsStatusInput{}, errors.New("no data supplied to parse") diff --git a/pkg/dtos/component_status_output.go b/pkg/dtos/component_status_output.go index 5af3f90..7ad3367 100644 --- a/pkg/dtos/component_status_output.go +++ b/pkg/dtos/component_status_output.go @@ -45,6 +45,14 @@ type ComponentsStatusOutput struct { Components []ComponentStatusOutput `json:"components"` } +// ExportComponentStatusOutput marshals a ComponentStatusOutput struct to JSON bytes. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentStatusOutput struct to be marshaled +// +// Returns: +// - JSON byte array representation of the output, or error if marshaling fails func ExportComponentStatusOutput(s *zap.SugaredLogger, output ComponentStatusOutput) ([]byte, error) { data, err := json.Marshal(output) if err != nil { @@ -54,6 +62,14 @@ func ExportComponentStatusOutput(s *zap.SugaredLogger, output ComponentStatusOut return data, nil } +// ParseComponentStatusOutput unmarshals JSON bytes into a ComponentStatusOutput struct. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: JSON byte array to be unmarshaled +// +// Returns: +// - ComponentStatusOutput struct populated from JSON, or error if unmarshaling fails or input is empty func ParseComponentStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentStatusOutput, error) { if len(output) == 0 { return ComponentStatusOutput{}, errors.New("no data supplied to parse") @@ -67,15 +83,32 @@ func ParseComponentStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentS return data, nil } +// ExportComponentsStatusOutput marshals a ComponentsStatusOutput struct (containing multiple components) to JSON bytes. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentsStatusOutput struct containing an array of component statuses +// +// Returns: +// - JSON byte array representation of the output, or error if marshaling fails func ExportComponentsStatusOutput(s *zap.SugaredLogger, output ComponentsStatusOutput) ([]byte, error) { data, err := json.Marshal(output) if err != nil { - s.Errorf("Parse failure: %v", err) - return nil, errors.New("failed to produce JSON ") + s.Errorf("Marshall failure: %v", err) + return nil, errors.New("failed to produce JSON") } return data, nil } +// ParseComponentsStatusOutput unmarshals JSON bytes into a ComponentsStatusOutput struct. +// Used for parsing batch component status responses containing multiple components. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: JSON byte array to be unmarshaled +// +// Returns: +// - ComponentsStatusOutput struct with array of component statuses, or error if unmarshaling fails or input is empty func ParseComponentsStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentsStatusOutput, error) { if len(output) == 0 { return ComponentsStatusOutput{}, errors.New("no data supplied to parse") diff --git a/pkg/models/all_urls_test.go b/pkg/models/all_urls_test.go index 29cf3f7..07d341f 100644 --- a/pkg/models/all_urls_test.go +++ b/pkg/models/all_urls_test.go @@ -18,15 +18,16 @@ package models import ( "context" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" myconfig "scanoss.com/components/pkg/config" - "testing" ) -// setupTest initializes all necessary components for testing +// setupTest initialises all necessary components for testing func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -41,7 +42,7 @@ func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/models/common_test.go b/pkg/models/common_test.go index 11a81ca..30be119 100644 --- a/pkg/models/common_test.go +++ b/pkg/models/common_test.go @@ -80,7 +80,7 @@ func TestRunQueriesInParallel(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/models/component_status.go b/pkg/models/component_status.go index 65af0b2..85f4636 100644 --- a/pkg/models/component_status.go +++ b/pkg/models/component_status.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -68,29 +68,24 @@ func (m *ComponentStatusModel) GetComponentStatusByPurlAndVersion(purlString, ve m.s.Errorf("Please specify a valid Purl String to query") return nil, errors.New("please specify a valid Purl String to query") } - purl, err := purlhelper.PurlFromString(purlString) if err != nil { return nil, err } - purlName, err := purlhelper.PurlNameFromString(purlString) if err != nil { return nil, err } - var status ComponentVersionStatus - // Query to get both version and component status query := ` - select distinct au.purl_name, au."version", au.indexed_date, au.version_status, au.version_status_change_date - from - all_urls au, - mines m - where - au.mine_id = m.id and au.purl_name = $1 and m.purl_type = $2 and au."version" = $3 + SELECT DISTINCT au.purl_name, au."version", au.indexed_date, au.version_status, au.version_status_change_date + FROM + all_urls au, + mines m + WHERE + au.mine_id = m.id AND au.purl_name = $1 AND m.purl_type = $2 AND au."version" = $3 ` - var results []ComponentVersionStatus err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type, version) if err != nil { @@ -102,7 +97,6 @@ func (m *ComponentStatusModel) GetComponentStatusByPurlAndVersion(purlString, ve return nil, fmt.Errorf("component version not found") } status = results[0] - m.s.Debugf("Found status for %v version %v", purlName, version) return &status, nil } @@ -113,36 +107,31 @@ func (m *ComponentStatusModel) GetComponentStatusByPurl(purlString string) (*Com m.s.Errorf("Please specify a valid Purl String to query") return nil, errors.New("please specify a valid Purl String to query") } - purl, err := purlhelper.PurlFromString(purlString) if err != nil { return nil, err } - purlName, err := purlhelper.PurlNameFromString(purlString) if err != nil { return nil, err } - var status ComponentProjectStatus - // Query to get both version and component status for the latest version query := ` - select distinct - p.component, - p.first_indexed_date, - p.last_indexed_date , - p.status, - p.status_change_date - from - projects p, - mines m - where - p.mine_id = m.id - and p.purl_name = $1 - and m.purl_type = $2; + SELECT DISTINCT + p.component, + p.first_indexed_date, + p.last_indexed_date , + p.status, + p.status_change_date + FROM + projects p, + mines m + WHERE + p.mine_id = m.id + AND p.purl_name = $1 + AND m.purl_type = $2; ` - var results []ComponentProjectStatus err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type) if err != nil { @@ -154,7 +143,6 @@ func (m *ComponentStatusModel) GetComponentStatusByPurl(purlString string) (*Com return nil, fmt.Errorf("component not found") } status = results[0] - m.s.Debugf("Found status for %v", purlName) return &status, nil } @@ -165,22 +153,17 @@ func (m *ComponentStatusModel) GetProjectStatusByPurl(purlString string) (*Compo m.s.Errorf("Please specify a valid Purl String to query") return nil, errors.New("please specify a valid Purl String to query") } - purl, err := purlhelper.PurlFromString(purlString) if err != nil { return nil, err } - purlName, err := purlhelper.PurlNameFromString(purlString) if err != nil { return nil, err } - var status ComponentProjectStatus - query := ` SELECT DISTINCT - p.purl_name, p.component, p.first_indexed_date, @@ -193,7 +176,6 @@ func (m *ComponentStatusModel) GetProjectStatusByPurl(purlString string) (*Compo AND m.purl_type = $2 LIMIT 1 ` - var results []ComponentProjectStatus err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type) if err != nil { @@ -205,7 +187,6 @@ func (m *ComponentStatusModel) GetProjectStatusByPurl(purlString string) (*Compo return nil, fmt.Errorf("component not found") } status = results[0] - m.s.Debugf("Found project status for %v", purlName) return &status, nil } diff --git a/pkg/models/component_status_test.go b/pkg/models/component_status_test.go index 0ba990a..0b766c8 100644 --- a/pkg/models/component_status_test.go +++ b/pkg/models/component_status_test.go @@ -45,7 +45,7 @@ func TestGetComponentStatusByPurlAndVersion(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -119,7 +119,7 @@ func TestGetComponentStatusByPurl(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -179,7 +179,7 @@ func TestGetProjectStatusByPurl(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/models/components_test.go b/pkg/models/components_test.go index b2a27a1..8ac959e 100644 --- a/pkg/models/components_test.go +++ b/pkg/models/components_test.go @@ -44,7 +44,7 @@ func TestComponentsModel(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index ab7ab24..0a5437d 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -175,20 +175,17 @@ func telemetryCompVersionRequestTime(ctx context.Context, config *myconfig.Serve func (d componentServer) GetComponentStatus(ctx context.Context, request *common.ComponentRequest) (*pb.ComponentStatusResponse, error) { s := ctxzap.Extract(ctx).Sugar() s.Info("Processing component status request...") - // Verify the input request if len(request.Purl) == 0 { s.Error("No purl supplied") return &pb.ComponentStatusResponse{}, se.NewBadRequestError("No purl supplied", nil) } - // Convert the request to internal DTO dtoRequest, err := convertComponentStatusInput(s, request) if err != nil { s.Errorf("Failed to convert component status input: %v", err) return &pb.ComponentStatusResponse{}, err } - // Create the use case compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoOutput, err := compUc.GetComponentStatus(dtoRequest) @@ -196,14 +193,12 @@ func (d componentServer) GetComponentStatus(ctx context.Context, request *common s.Errorf("Failed to get component status: %v", err) return &pb.ComponentStatusResponse{}, err } - // Convert the output to protobuf statusResponse, err := convertComponentStatusOutput(s, dtoOutput) if err != nil { s.Errorf("Failed to convert component status output: %v", err) return &pb.ComponentStatusResponse{}, errors.New("problems encountered extracting component status data") } - return statusResponse, nil } @@ -211,7 +206,6 @@ func (d componentServer) GetComponentStatus(ctx context.Context, request *common func (d componentServer) GetComponentsStatus(ctx context.Context, request *common.ComponentsRequest) (*pb.ComponentsStatusResponse, error) { s := ctxzap.Extract(ctx).Sugar() s.Info("Processing components status request...") - // Verify the input request if len(request.Components) == 0 { status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No components supplied", nil)) @@ -219,7 +213,6 @@ func (d componentServer) GetComponentsStatus(ctx context.Context, request *commo status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} return &pb.ComponentsStatusResponse{Status: status}, nil } - // Convert the request to internal DTO dtoRequest, err := convertComponentsStatusInput(s, request) if err != nil { @@ -228,7 +221,6 @@ func (d componentServer) GetComponentsStatus(ctx context.Context, request *commo status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} return &pb.ComponentsStatusResponse{Status: status}, nil } - // Create the use case compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoOutput, err := compUc.GetComponentsStatus(dtoRequest) @@ -238,7 +230,6 @@ func (d componentServer) GetComponentsStatus(ctx context.Context, request *commo status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} return &pb.ComponentsStatusResponse{Status: status}, nil } - // Convert the output to protobuf statusResponse, err := convertComponentsStatusOutput(s, dtoOutput) if err != nil { @@ -250,7 +241,6 @@ func (d componentServer) GetComponentsStatus(ctx context.Context, request *commo Server: &common.StatusResponse_Server{Version: d.config.App.Version}, }}, nil } - // Set the status and respond with the data return &pb.ComponentsStatusResponse{ Components: statusResponse.Components, diff --git a/pkg/service/component_service_test.go b/pkg/service/component_service_test.go index 8e5600c..39f70e6 100644 --- a/pkg/service/component_service_test.go +++ b/pkg/service/component_service_test.go @@ -44,7 +44,7 @@ func TestComponentServer_Echo(t *testing.T) { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer models.CloseDB(db) - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -103,7 +103,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -184,7 +184,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -264,7 +264,7 @@ func TestComponentServer_GetComponentStatus(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/service/component_support.go b/pkg/service/component_support.go index fccfee0..b55223f 100644 --- a/pkg/service/component_support.go +++ b/pkg/service/component_support.go @@ -28,6 +28,15 @@ func setupMetrics() { oltpMetrics.compVersionHistogram, _ = meter.Int64Histogram("comp.versions.req_time", metric.WithDescription("The time taken to run a comp versions request (ms)")) } +// convertSearchComponentInput converts a gRPC CompSearchRequest into a ComponentSearchInput DTO. +// It marshals the gRPC request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: gRPC component search request +// +// Returns: +// - ComponentSearchInput DTO or BadRequestError if conversion fails func convertSearchComponentInput(s *zap.SugaredLogger, request *pb.CompSearchRequest) (dtos.ComponentSearchInput, error) { data, err := json.Marshal(request) if err != nil { @@ -40,6 +49,15 @@ func convertSearchComponentInput(s *zap.SugaredLogger, request *pb.CompSearchReq return dtoRequest, nil } +// convertSearchComponentOutput converts a ComponentsSearchOutput DTO into a gRPC CompSearchResponse. +// It marshals the DTO to JSON and then unmarshals it into the gRPC response format. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentsSearchOutput DTO containing search results +// +// Returns: +// - gRPC CompSearchResponse or error if conversion fails func convertSearchComponentOutput(s *zap.SugaredLogger, output dtos.ComponentsSearchOutput) (*pb.CompSearchResponse, error) { data, err := json.Marshal(output) if err != nil { @@ -55,6 +73,15 @@ func convertSearchComponentOutput(s *zap.SugaredLogger, output dtos.ComponentsSe return &compResp, nil } +// convertCompVersionsInput converts a gRPC CompVersionRequest into a ComponentVersionsInput DTO. +// It marshals the gRPC request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: gRPC component version request +// +// Returns: +// - ComponentVersionsInput DTO or BadRequestError if conversion fails func convertCompVersionsInput(s *zap.SugaredLogger, request *pb.CompVersionRequest) (dtos.ComponentVersionsInput, error) { data, err := json.Marshal(request) if err != nil { @@ -67,6 +94,15 @@ func convertCompVersionsInput(s *zap.SugaredLogger, request *pb.CompVersionReque return dtoRequest, nil } +// convertCompVersionsOutput converts a ComponentVersionsOutput DTO into a gRPC CompVersionResponse. +// It marshals the DTO to JSON and then unmarshals it into the gRPC response format. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentVersionsOutput DTO containing component version data +// +// Returns: +// - gRPC CompVersionResponse or error if conversion fails func convertCompVersionsOutput(s *zap.SugaredLogger, output dtos.ComponentVersionsOutput) (*pb.CompVersionResponse, error) { data, err := json.Marshal(output) if err != nil { @@ -82,6 +118,16 @@ func convertCompVersionsOutput(s *zap.SugaredLogger, output dtos.ComponentVersio return &compResp, nil } +// convertComponentStatusInput converts a gRPC component status request into a ComponentStatusInput DTO. +// It accepts an interface{} to support both REST and gRPC request formats. +// It marshals the request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: Generic request interface (gRPC or REST format) +// +// Returns: +// - ComponentStatusInput DTO or BadRequestError if conversion fails func convertComponentStatusInput(s *zap.SugaredLogger, request interface{}) (dtos.ComponentStatusInput, error) { data, err := json.Marshal(request) if err != nil { @@ -94,6 +140,16 @@ func convertComponentStatusInput(s *zap.SugaredLogger, request interface{}) (dto return dtoRequest, nil } +// convertComponentStatusOutput converts a ComponentStatusOutput DTO into a gRPC ComponentStatusResponse. +// It manually constructs the gRPC response structure, handling both success and error cases. +// The function handles optional fields like StatusChangeDate and VersionStatus appropriately. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentStatusOutput DTO containing component and version status data +// +// Returns: +// - gRPC ComponentStatusResponse with populated fields, or error if conversion fails func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentStatusOutput) (*pb.ComponentStatusResponse, error) { response := &pb.ComponentStatusResponse{ @@ -141,6 +197,16 @@ func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentSta return response, nil } +// convertComponentsStatusInput converts a gRPC components status request into a ComponentsStatusInput DTO. +// It accepts an interface{} to support both REST and gRPC request formats for batch status requests. +// It marshals the request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: Generic request interface (gRPC or REST format) containing multiple component status requests +// +// Returns: +// - ComponentsStatusInput DTO or BadRequestError if conversion fails func convertComponentsStatusInput(s *zap.SugaredLogger, request interface{}) (dtos.ComponentsStatusInput, error) { data, err := json.Marshal(request) if err != nil { @@ -153,6 +219,16 @@ func convertComponentsStatusInput(s *zap.SugaredLogger, request interface{}) (dt return dtoRequest, nil } +// convertComponentsStatusOutput converts a ComponentsStatusOutput DTO into a gRPC ComponentsStatusResponse. +// It iterates through multiple component status results and converts each one using convertComponentStatusOutput. +// This function handles batch status responses for multiple components. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentsStatusOutput DTO containing multiple component status results +// +// Returns: +// - gRPC ComponentsStatusResponse with all converted component status entries, or error if conversion fails func convertComponentsStatusOutput(s *zap.SugaredLogger, output dtos.ComponentsStatusOutput) (*pb.ComponentsStatusResponse, error) { var statusResp pb.ComponentsStatusResponse diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index 27d66f0..bba46fa 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -59,7 +59,6 @@ func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *da func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (dtos.ComponentsSearchOutput, error) { var err error var searchResults []models.Component - if len(request.Search) != 0 { searchResults, err = c.components.GetComponents(request.Search, request.Package, request.Limit, request.Offset) } else if len(request.Component) != 0 && len(request.Vendor) == 0 { @@ -92,12 +91,10 @@ func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (d } func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInput) (dtos.ComponentVersionsOutput, error) { - if len(request.Purl) == 0 { c.s.Errorf("The request does not contains purl to retrieve component versions") return dtos.ComponentVersionsOutput{}, errors.New("the request does not contains purl to retrieve component versions") } - allUrls, err := c.allUrl.GetUrlsByPurlString(request.Purl, request.Limit) if err != nil { c.s.Errorf("Problem encountered gettings URLs versions for: %v - %v.", request.Purl, err) @@ -107,17 +104,14 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp if err != nil { c.s.Warnf("Problem encountered generating output component versions for: %v - %v.", request.Purl, err) } - purlName := purl.Name if purl.Type == "github" { purlName = fmt.Sprintf("%s/%s", purl.Namespace, purl.Name) } - projectURL, err := purlhelper.ProjectUrl(purlName, purl.Type) if err != nil { c.s.Warnf("Problem generating the project URL: %v - %v.", request.Purl, err) } - var output dtos.ComponentOutput output.Purl = request.Purl if len(allUrls) > 0 { @@ -158,7 +152,6 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) c.s.Errorf("The request does not contain purl to retrieve component status") return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) } - results := cmpHelper.GetComponentsVersion(cmpHelper.ComponentVersionCfg{ MaxWorkers: 1, Ctx: c.ctx, @@ -168,103 +161,120 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) {Purl: request.Purl, Requirement: request.Requirement}, }, }) - if len(results) > 0 { - // Component and Version exists or requirement met - switch results[0].Status.StatusCode { - case domain.Success: - statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(results[0].Purl) - if errComp != nil { - return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving Component level data", errors.New("Error retrieving Component Level Data")) - } + return c.handleComponentStatusResult(request, results[0]) + } + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) +} - var statusVersion *models.ComponentVersionStatus - var errVersion error - if len(results[0].Version) > 0 { - statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(request.Purl, results[0].Version) - } else if len(results[0].Requirement) > 0 { - statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(request.Purl, results[0].Requirement) - } - if errVersion != nil { - c.s.Warnf("Problems getting version level status data for: %v - %v", request.Purl, errVersion) - } +// handleComponentStatusResult routes the component status result to the appropriate handler based on status code. +func (c ComponentUseCase) handleComponentStatusResult(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + switch result.Status.StatusCode { + case domain.Success: + return c.handleSuccessStatus(request, result) + case domain.VersionNotFound: + return c.handleVersionNotFound(request, result) + case domain.InvalidPurl, domain.ComponentNotFound: + return c.handleErrorStatus(result) + default: + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("unknown status code", errors.New("unknown status code")) + } +} - output := dtos.ComponentStatusOutput{ - Purl: request.Purl, - Name: statComponent.Component, - Requirement: request.Requirement, - ComponentStatus: &dtos.ComponentStatusInfo{ - Status: c.statusMapper.MapStatus(statComponent.Status.String), - RepositoryStatus: statComponent.Status.String, - FirstIndexedDate: statComponent.FirstIndexedDate.String, - LastIndexedDate: statComponent.LastIndexedDate.String, - }, - } - if statusVersion != nil { - output.VersionStatus = &dtos.VersionStatusOutput{ - Version: statusVersion.Version, - Status: c.statusMapper.MapStatus(statusVersion.VersionStatus.String), - RepositoryStatus: statusVersion.VersionStatus.String, - IndexedDate: statusVersion.IndexedDate.String, - } - if statusVersion.VersionStatusChangeDate.String != "" { - output.VersionStatus.StatusChangeDate = statusVersion.VersionStatusChangeDate.String - } - } - if statComponent.StatusChangeDate.String != "" { - output.ComponentStatus.StatusChangeDate = statComponent.StatusChangeDate.String - } +// handleSuccessStatus handles the case where both component and version are found. +func (c ComponentUseCase) handleSuccessStatus(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(result.Purl) + if errComp != nil { + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving Component level data", errors.New("Error retrieving Component Level Data")) + } + output := dtos.ComponentStatusOutput{ + Purl: request.Purl, + Name: statComponent.Component, + Requirement: request.Requirement, + ComponentStatus: c.buildComponentStatusInfo(statComponent), + } + // Try to get version-specific status + statusVersion := c.getVersionStatus(request.Purl, result) + if statusVersion != nil { + output.VersionStatus = c.buildVersionStatusOutput(statusVersion) + } + return output, nil +} - return output, nil +// handleVersionNotFound handles the case where component exists but the version is not found. +func (c ComponentUseCase) handleVersionNotFound(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(result.Purl) + if errComp != nil { + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving information", errors.New("Error retrieving information")) + } + return dtos.ComponentStatusOutput{ + Purl: request.Purl, + Name: statComponent.Component, + Requirement: request.Requirement, + VersionStatus: &dtos.VersionStatusOutput{ + Version: request.Requirement, + ErrorMessage: &result.Status.Message, + ErrorCode: &result.Status.StatusCode, + }, + ComponentStatus: c.buildComponentStatusInfo(statComponent), + }, nil +} - case domain.VersionNotFound: // Valid component but VERSION NOT FOUND - statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(results[0].Purl) - if errComp != nil { - return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving information", errors.New("Error retrieving information")) - } - output := dtos.ComponentStatusOutput{ - Purl: request.Purl, - Name: statComponent.Component, - Requirement: request.Requirement, - VersionStatus: &dtos.VersionStatusOutput{ - Version: request.Requirement, - ErrorMessage: &results[0].Status.Message, - ErrorCode: &results[0].Status.StatusCode, - }, - ComponentStatus: &dtos.ComponentStatusInfo{Status: c.statusMapper.MapStatus(statComponent.Status.String), - RepositoryStatus: statComponent.Status.String, - FirstIndexedDate: statComponent.FirstIndexedDate.String, - LastIndexedDate: statComponent.LastIndexedDate.String, - }, - } - return output, nil - case domain.InvalidPurl: // The requested purl is invalid, minimun data retrieved - errorStatus := dtos.ComponentStatusOutput{ - Purl: results[0].Purl, - Name: "", - Requirement: results[0].Requirement, - ComponentStatus: &dtos.ComponentStatusInfo{ - ErrorMessage: &results[0].Status.Message, - ErrorCode: &results[0].Status.StatusCode, - }, - } - return errorStatus, nil +// handleErrorStatus handles error cases like InvalidPurl or ComponentNotFound. +func (c ComponentUseCase) handleErrorStatus(result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + return dtos.ComponentStatusOutput{ + Purl: result.Purl, + Name: "", + Requirement: result.Requirement, + ComponentStatus: &dtos.ComponentStatusInfo{ + ErrorMessage: &result.Status.Message, + ErrorCode: &result.Status.StatusCode, + }, + }, nil +} - case domain.ComponentNotFound: // Component not found on DB, minimun data retrieved - errorStatus := dtos.ComponentStatusOutput{ - Purl: results[0].Purl, - Name: "", - Requirement: results[0].Requirement, - ComponentStatus: &dtos.ComponentStatusInfo{ - ErrorMessage: &results[0].Status.Message, - ErrorCode: &results[0].Status.StatusCode, - }, - } - return errorStatus, nil +// buildComponentStatusInfo constructs a ComponentStatusInfo from a ComponentProjectStatus model. +func (c ComponentUseCase) buildComponentStatusInfo(statComponent *models.ComponentProjectStatus) *dtos.ComponentStatusInfo { + info := &dtos.ComponentStatusInfo{ + Status: c.statusMapper.MapStatus(statComponent.Status.String), + RepositoryStatus: statComponent.Status.String, + FirstIndexedDate: statComponent.FirstIndexedDate.String, + LastIndexedDate: statComponent.LastIndexedDate.String, + } + if statComponent.StatusChangeDate.String != "" { + info.StatusChangeDate = statComponent.StatusChangeDate.String + } + return info +} - } +// getVersionStatus retrieves version-specific status information. +func (c ComponentUseCase) getVersionStatus(purl string, result cmpHelper.Component) *models.ComponentVersionStatus { + var statusVersion *models.ComponentVersionStatus + var errVersion error + if len(result.Version) > 0 { + statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(purl, result.Version) + } else if len(result.Requirement) > 0 { + statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(purl, result.Requirement) } - return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) + if errVersion != nil { + c.s.Warnf("Problems getting version level status data for: %v - %v", purl, errVersion) + return nil + } + return statusVersion +} + +// buildVersionStatusOutput constructs a VersionStatusOutput from a ComponentVersionStatus model. +func (c ComponentUseCase) buildVersionStatusOutput(statusVersion *models.ComponentVersionStatus) *dtos.VersionStatusOutput { + output := &dtos.VersionStatusOutput{ + Version: statusVersion.Version, + Status: c.statusMapper.MapStatus(statusVersion.VersionStatus.String), + RepositoryStatus: statusVersion.VersionStatus.String, + IndexedDate: statusVersion.IndexedDate.String, + } + if statusVersion.VersionStatusChangeDate.String != "" { + output.StatusChangeDate = statusVersion.VersionStatusChangeDate.String + } + return output } func (c ComponentUseCase) GetComponentsStatus(request dtos.ComponentsStatusInput) (dtos.ComponentsStatusOutput, error) { @@ -272,10 +282,8 @@ func (c ComponentUseCase) GetComponentsStatus(request dtos.ComponentsStatusInput c.s.Errorf("The request does not contain any components to retrieve status") return dtos.ComponentsStatusOutput{}, se.NewBadRequestError("components array is required", errors.New("components array is required")) } - var output dtos.ComponentsStatusOutput output.Components = make([]dtos.ComponentStatusOutput, 0, len(request.Components)) - // Process each component request for _, componentRequest := range request.Components { componentStatus, err := c.GetComponentStatus(componentRequest) @@ -283,7 +291,6 @@ func (c ComponentUseCase) GetComponentsStatus(request dtos.ComponentsStatusInput // For batch requests, we continue even if one component fails // Add an error entry for this component c.s.Warnf("Failed to get status for component: %v - %v", componentRequest.Purl, err) - errorMsg := err.Error() errorStatus := dtos.ComponentStatusOutput{ Purl: componentRequest.Purl, @@ -298,6 +305,5 @@ func (c ComponentUseCase) GetComponentsStatus(request dtos.ComponentsStatusInput output.Components = append(output.Components, componentStatus) } } - return output, nil } diff --git a/pkg/usecase/component_test.go b/pkg/usecase/component_test.go index 40f7f89..2cc9b4f 100644 --- a/pkg/usecase/component_test.go +++ b/pkg/usecase/component_test.go @@ -48,7 +48,7 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -106,7 +106,7 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -170,7 +170,7 @@ func TestComponentUseCase_GetComponentStatus(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -237,7 +237,7 @@ func TestComponentUseCase_GetComponentsStatus(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -304,7 +304,7 @@ func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil) + myConfig, err := myconfig.NewServerConfig(nil, nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } From ed59978fba8fe65f9f03d05c848fa9adfb08af9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Go=C3=B1i?= Date: Thu, 26 Mar 2026 07:08:03 -0300 Subject: [PATCH 10/13] Applied some linter errors (mostly bounded to this branch) --- pkg/config/server_config.go | 8 ++++---- pkg/config/server_config_integration_test.go | 15 ++++++++++++--- pkg/dtos/component_version_output_test.go | 6 ++---- pkg/models/common_test.go | 5 ++--- pkg/models/components.go | 8 +++----- pkg/service/component_service.go | 4 ++-- pkg/service/component_support.go | 8 ++++++-- pkg/usecase/component.go | 4 ++-- 8 files changed, 33 insertions(+), 25 deletions(-) diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 13e47b3..484cfbc 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -30,7 +30,7 @@ const ( ) // parseStatusMappingString converts a string to interface{} for StatusMapper -// It handles both JSON object format (from config file) and JSON string format (from env var) +// It handles both JSON object format (from config file) and JSON string format (from env var). func parseStatusMappingString(s string) interface{} { if s == "" { return nil @@ -44,7 +44,7 @@ func parseStatusMappingString(s string) interface{} { return s } -// ServerConfig is configuration for Server +// ServerConfig is configuration for Server. type ServerConfig struct { App struct { Name string `env:"APP_NAME"` @@ -117,7 +117,7 @@ func NewServerConfig(feeders []config.Feeder, logger *zap.SugaredLogger) (*Serve return &cfg, nil } -// setServerConfigDefaults attempts to set reasonable defaults for the server config +// setServerConfigDefaults attempts to set reasonable defaults for the server config. func setServerConfigDefaults(cfg *ServerConfig) { cfg.App.Name = "SCANOSS Component Server" cfg.App.GRPCPort = defaultGrpcPort @@ -137,7 +137,7 @@ func setServerConfigDefaults(cfg *ServerConfig) { cfg.Telemetry.OltpExporter = "0.0.0.0:4317" // Default OTEL OLTP gRPC Exporter endpoint } -// GetStatusMapper returns the status mapper for mapping database statuses to classified statuses +// GetStatusMapper returns the status mapper for mapping database statuses to classified statuses. func (cfg *ServerConfig) GetStatusMapper() *StatusMapper { return cfg.statusMapper } diff --git a/pkg/config/server_config_integration_test.go b/pkg/config/server_config_integration_test.go index c569619..9e94986 100644 --- a/pkg/config/server_config_integration_test.go +++ b/pkg/config/server_config_integration_test.go @@ -33,7 +33,10 @@ func TestServerConfig_StatusMapping_FromEnv(t *testing.T) { } defer zlog.SyncZap() envValue := `{"unlisted":"custom-removed","yanked":"custom-yanked"}` - os.Setenv("STATUS_MAPPING", envValue) + errEnv := os.Setenv("STATUS_MAPPING", envValue) + if errEnv != nil { + t.Fatalf("Could not set env variable: %v", errEnv) + } defer os.Unsetenv("STATUS_MAPPING") cfg, err := NewServerConfig(nil, nil) if err != nil { @@ -69,7 +72,10 @@ func TestServerConfig_StatusMapping_DefaultWhenNotSet(t *testing.T) { t.Fatalf("Failed to initialise logger: %v", err) } defer zlog.SyncZap() - os.Unsetenv("STATUS_MAPPING") + errEnv := os.Unsetenv("STATUS_MAPPING") + if errEnv != nil { + t.Fatalf("Could not set env variable: %v", errEnv) + } cfg, err := NewServerConfig(nil, nil) if err != nil { t.Fatalf("Failed to load config: %v", err) @@ -102,7 +108,10 @@ func TestServerConfig_StatusMapping_WithProvidedLogger(t *testing.T) { } defer zlog.SyncZap() envValue := `{"test-status":"test-mapped"}` - os.Setenv("STATUS_MAPPING", envValue) + errEnv := os.Setenv("STATUS_MAPPING", envValue) + if errEnv != nil { + t.Fatalf("Could not set env variable: %v", errEnv) + } defer os.Unsetenv("STATUS_MAPPING") cfg, err := NewServerConfig(nil, zlog.S) if err != nil { diff --git a/pkg/dtos/component_version_output_test.go b/pkg/dtos/component_version_output_test.go index 6c84a55..84dc123 100644 --- a/pkg/dtos/component_version_output_test.go +++ b/pkg/dtos/component_version_output_test.go @@ -3,10 +3,11 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) func TestParseComponentVersionsOutput(t *testing.T) { @@ -91,9 +92,7 @@ func TestParseComponentVersionsOutput(t *testing.T) { if err == nil { t.Errorf("Expected an error for empty input") } - } - func TestExportComponentVersionsOutput(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -142,5 +141,4 @@ func TestExportComponentVersionsOutput(t *testing.T) { t.Errorf("dtos.ExportComponentVersionsOutput() error = %v", err) } fmt.Println("Exported output data: ", data) - } diff --git a/pkg/models/common_test.go b/pkg/models/common_test.go index 30be119..bb53427 100644 --- a/pkg/models/common_test.go +++ b/pkg/models/common_test.go @@ -19,6 +19,8 @@ package models import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" @@ -26,7 +28,6 @@ import ( zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" myconfig "scanoss.com/components/pkg/config" - "testing" ) func TestDbLoad(t *testing.T) { @@ -74,8 +75,6 @@ func TestRunQueriesInParallel(t *testing.T) { s := ctxzap.Extract(ctx).Sugar() db := sqliteSetup(t) // Setup SQL Lite DB defer CloseDB(db) - //conn := sqliteConn(t, ctx, db) // Get a connection from the pool - //defer CloseConn(conn) err = LoadTestSQLData(db, ctx, nil) if err != nil { t.Fatalf("failed to load SQL test data: %v", err) diff --git a/pkg/models/components.go b/pkg/models/components.go index 8b274ba..964a4d7 100644 --- a/pkg/models/components.go +++ b/pkg/models/components.go @@ -19,9 +19,10 @@ package models import ( "context" "errors" + "strings" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" "go.uber.org/zap" - "strings" ) var defaultPurlType = "github" @@ -68,7 +69,7 @@ func preProcessQueryJob(qListIn []QueryJob, purlType string) ([]QueryJob, error) } for i := range qList { - //Adds or remove the ORDER BY clause in SQL query + // Adds or remove the ORDER BY clause in SQL query qList[i].Query = strings.Replace(qList[i].Query, "#ORDER", mapPurlTypeToOrderByClause[purlType], 1) qList[i].Query = strings.TrimRight(qList[i].Query, " ") } @@ -293,19 +294,16 @@ func (m *ComponentModel) GetComponentsByVendorType(vendorName, purlType string, Args: []any{"%" + vendorName, purlType, 1, offset}, }, } - queryJobs, err := preProcessQueryJob(queryJobs, purlType) if err != nil { return []Component{}, err } - allComponents, _ := RunQueries[Component](m.q, m.ctx, queryJobs) allComponents = RemoveDuplicated[Component](allComponents) if limit < len(allComponents) { allComponents = allComponents[:limit] } - return allComponents, nil } diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index 0a5437d..5dcb60b 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -110,14 +110,14 @@ func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.C requestStartTime := time.Now() // Capture the scan start time s := ctxzap.Extract(ctx).Sugar() s.Info("Processing component versions request...") - //Verify the input request + // Verify the input request if len(request.Purl) == 0 { status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No purl supplied", nil)) status.Db = d.getDBVersion() status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} return &pb.CompVersionResponse{Status: status}, nil } - //Convert the request to internal DTO + // Convert the request to internal DTO dtoRequest, err := convertCompVersionsInput(s, request) if err != nil { status := se.HandleServiceError(ctx, s, err) diff --git a/pkg/service/component_support.go b/pkg/service/component_support.go index b55223f..b5010a3 100644 --- a/pkg/service/component_support.go +++ b/pkg/service/component_support.go @@ -232,10 +232,14 @@ func convertComponentsStatusInput(s *zap.SugaredLogger, request interface{}) (dt func convertComponentsStatusOutput(s *zap.SugaredLogger, output dtos.ComponentsStatusOutput) (*pb.ComponentsStatusResponse, error) { var statusResp pb.ComponentsStatusResponse + var someErr error = nil for _, c := range output.Components { - cs, _ := convertComponentStatusOutput(s, c) + cs, errComp := convertComponentStatusOutput(s, c) + if errComp != nil { + someErr = errComp + } statusResp.Components = append(statusResp.Components, cs) } - return &statusResp, nil + return &statusResp, someErr } diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index bba46fa..07106d0 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -185,7 +185,7 @@ func (c ComponentUseCase) handleComponentStatusResult(request dtos.ComponentStat func (c ComponentUseCase) handleSuccessStatus(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(result.Purl) if errComp != nil { - return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving Component level data", errors.New("Error retrieving Component Level Data")) + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("error retrieving Component level data", errors.New("error retrieving Component Level Data")) } output := dtos.ComponentStatusOutput{ Purl: request.Purl, @@ -205,7 +205,7 @@ func (c ComponentUseCase) handleSuccessStatus(request dtos.ComponentStatusInput, func (c ComponentUseCase) handleVersionNotFound(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(result.Purl) if errComp != nil { - return dtos.ComponentStatusOutput{}, se.NewBadRequestError("Error retrieving information", errors.New("Error retrieving information")) + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("error retrieving information", errors.New("error retrieving information")) } return dtos.ComponentStatusOutput{ Purl: request.Purl, From bc372fd71fa0675de02a028c2ef2b01992219357 Mon Sep 17 00:00:00 2001 From: Sean Egan Date: Thu, 26 Mar 2026 15:25:29 +0000 Subject: [PATCH 11/13] simply status mapper initialisation --- pkg/cmd/server.go | 21 +++++++++----------- pkg/config/server_config.go | 21 +++++++++++--------- pkg/config/server_config_integration_test.go | 11 +++++----- pkg/config/server_config_test.go | 11 +++++----- pkg/config/status_mapper.go | 20 +++++++++++++------ pkg/models/all_urls_test.go | 2 +- pkg/models/common_test.go | 2 +- pkg/models/component_status_test.go | 6 +++--- pkg/models/components_test.go | 2 +- pkg/service/component_service_test.go | 13 ++++++------ pkg/usecase/component_test.go | 14 ++++++------- 11 files changed, 66 insertions(+), 57 deletions(-) diff --git a/pkg/cmd/server.go b/pkg/cmd/server.go index 7aa96a4..f3a1990 100644 --- a/pkg/cmd/server.go +++ b/pkg/cmd/server.go @@ -21,6 +21,10 @@ import ( _ "embed" "flag" "fmt" + "net/http" + "os" + "strings" + "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" _ "github.com/lib/pq" @@ -30,16 +34,13 @@ import ( gomodels "github.com/scanoss/go-models/pkg/models" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" - "net/http" - "os" myconfig "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/protocol/grpc" "scanoss.com/components/pkg/protocol/rest" "scanoss.com/components/pkg/service" - "strings" ) -//TODO: Now the config includes the app version. +//TODO: Now the config includes the app version. // This might be worth moving to the file pkg/config/server_config.go //go:generate bash ../../get_version.sh @@ -74,21 +75,17 @@ func getConfig() (*myconfig.ServerConfig, error) { return nil, err } } - - // Phase 1: Load config without logger to get logging settings - preConfig, err := myconfig.NewServerConfig(feeders, nil) + myConfig, err := myconfig.NewServerConfig(feeders) if err != nil { return nil, err } - // Initialize the application logger - err = zlog.SetupAppLogger(preConfig.App.Mode, preConfig.Logging.ConfigFile, preConfig.App.Debug) + err = zlog.SetupAppLogger(myConfig.App.Mode, myConfig.Logging.ConfigFile, myConfig.App.Debug) if err != nil { return nil, err } - - // Phase 2: Reload config with initialized logger for StatusMapper - myConfig, err := myconfig.NewServerConfig(feeders, zlog.S) + // Initialise the status mapping config + myConfig.InitStatusMapperConfig(zlog.S) return myConfig, err } diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 484cfbc..2beb181 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -21,6 +21,7 @@ import ( "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" "go.uber.org/zap" ) @@ -44,7 +45,7 @@ func parseStatusMappingString(s string) interface{} { return s } -// ServerConfig is configuration for Server. +// ServerConfig is a configuration for Server. type ServerConfig struct { App struct { Name string `env:"APP_NAME"` @@ -94,9 +95,7 @@ type ServerConfig struct { } // NewServerConfig loads all config options and return a struct for use. -// If logger is nil, uses zap.S() to get the global sugared logger. -// For production use, pass an initialized logger after calling zlog.SetupAppLogger(). -func NewServerConfig(feeders []config.Feeder, logger *zap.SugaredLogger) (*ServerConfig, error) { +func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { cfg := ServerConfig{} setServerConfigDefaults(&cfg) c := config.New() @@ -109,11 +108,6 @@ func NewServerConfig(feeders []config.Feeder, logger *zap.SugaredLogger) (*Serve if err != nil { return nil, err } - // Initialise the status mapper once at startup - if logger == nil { - logger = zap.S() // Fallback to global sugared logger - } - cfg.statusMapper = NewStatusMapper(logger, parseStatusMappingString(cfg.StatusMapping.Mapping)) return &cfg, nil } @@ -137,7 +131,16 @@ func setServerConfigDefaults(cfg *ServerConfig) { cfg.Telemetry.OltpExporter = "0.0.0.0:4317" // Default OTEL OLTP gRPC Exporter endpoint } +// InitStatusMapperConfig initialise the status mapper for mapping component statuses +func (cfg *ServerConfig) InitStatusMapperConfig(s *zap.SugaredLogger) { + cfg.statusMapper = NewStatusMapper(s, parseStatusMappingString(cfg.StatusMapping.Mapping)) +} + // GetStatusMapper returns the status mapper for mapping database statuses to classified statuses. func (cfg *ServerConfig) GetStatusMapper() *StatusMapper { + // Initialise the mapper if it wasn't done previously + if cfg.statusMapper == nil { + cfg.InitStatusMapperConfig(zlog.S) + } return cfg.statusMapper } diff --git a/pkg/config/server_config_integration_test.go b/pkg/config/server_config_integration_test.go index 9e94986..61fcb9b 100644 --- a/pkg/config/server_config_integration_test.go +++ b/pkg/config/server_config_integration_test.go @@ -38,13 +38,11 @@ func TestServerConfig_StatusMapping_FromEnv(t *testing.T) { t.Fatalf("Could not set env variable: %v", errEnv) } defer os.Unsetenv("STATUS_MAPPING") - cfg, err := NewServerConfig(nil, nil) + cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } - if cfg.statusMapper == nil { - t.Fatal("Expected statusMapper to be initialised") - } + // Allowing the GetStatusMapper to load the config mapper := cfg.GetStatusMapper() if mapper == nil { t.Fatal("Expected non-nil mapper from GetStatusMapper()") @@ -76,7 +74,7 @@ func TestServerConfig_StatusMapping_DefaultWhenNotSet(t *testing.T) { if errEnv != nil { t.Fatalf("Could not set env variable: %v", errEnv) } - cfg, err := NewServerConfig(nil, nil) + cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } @@ -113,10 +111,11 @@ func TestServerConfig_StatusMapping_WithProvidedLogger(t *testing.T) { t.Fatalf("Could not set env variable: %v", errEnv) } defer os.Unsetenv("STATUS_MAPPING") - cfg, err := NewServerConfig(nil, zlog.S) + cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } + cfg.InitStatusMapperConfig(zlog.S) mapper := cfg.GetStatusMapper() if mapper == nil { t.Fatal("Expected non-nil mapper from GetStatusMapper()") diff --git a/pkg/config/server_config_test.go b/pkg/config/server_config_test.go index f5d890b..df45f6a 100644 --- a/pkg/config/server_config_test.go +++ b/pkg/config/server_config_test.go @@ -18,10 +18,11 @@ package config import ( "fmt" - "github.com/golobby/config/v3" - "github.com/golobby/config/v3/pkg/feeder" "os" "testing" + + "github.com/golobby/config/v3" + "github.com/golobby/config/v3/pkg/feeder" ) // TestServerConfig verifies that NewServerConfig can load configuration from environment variables. @@ -35,7 +36,7 @@ func TestServerConfig(t *testing.T) { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } // Load config with nil feeders and nil logger (uses env vars and fallback logger) - cfg, err := NewServerConfig(nil, nil) + cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } @@ -61,7 +62,7 @@ func TestServerConfigDotEnv(t *testing.T) { dbUser := "env-user" var feeders []config.Feeder feeders = append(feeders, feeder.DotEnv{Path: "tests/dot-env"}) - cfg, err := NewServerConfig(feeders, nil) + cfg, err := NewServerConfig(feeders) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } @@ -81,7 +82,7 @@ func TestServerConfigJson(t *testing.T) { dbUser := "json-user" var feeders []config.Feeder feeders = append(feeders, feeder.Json{Path: "tests/env.json"}) - cfg, err := NewServerConfig(feeders, nil) + cfg, err := NewServerConfig(feeders) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } diff --git a/pkg/config/status_mapper.go b/pkg/config/status_mapper.go index 1d5944b..1ad9139 100644 --- a/pkg/config/status_mapper.go +++ b/pkg/config/status_mapper.go @@ -48,7 +48,9 @@ func NewStatusMapper(s *zap.SugaredLogger, mappingConfig interface{}) *StatusMap for key, value := range customMapping { mapper.mapping[strings.ToLower(key)] = value } - s.Infof("Loaded custom status mapping with %d entries", len(customMapping)) + if s != nil { + s.Infof("Loaded custom status mapping with %d entries", len(customMapping)) + } } return mapper @@ -67,7 +69,9 @@ func parseMappingConfig(s *zap.SugaredLogger, mappingConfig interface{}) map[str // Direct map format return v default: - s.Warnf("Unexpected mapping config type: %T, using defaults", mappingConfig) + if s != nil { + s.Warnf("Unexpected mapping config type: %T, using defaults", mappingConfig) + } return nil } } @@ -80,7 +84,9 @@ func parseJSONString(s *zap.SugaredLogger, jsonStr string) map[string]string { var result map[string]string err := json.Unmarshal([]byte(jsonStr), &result) if err != nil { - s.Warnf("Failed to parse STATUS_MAPPING JSON string, using defaults: %v", err) + if s != nil { + s.Warnf("Failed to parse STATUS_MAPPING JSON string, using defaults: %v", err) + } return nil } return result @@ -93,7 +99,9 @@ func convertInterfaceMap(s *zap.SugaredLogger, m map[string]interface{}) map[str if strValue, ok := value.(string); ok { result[key] = strValue } else { - s.Warnf("Skipping non-string value for key %q: %v (type: %T)", key, value, value) + if s != nil { + s.Warnf("Skipping non-string value for key %q: %v (type: %T)", key, value, value) + } } } return result @@ -105,12 +113,12 @@ func (m *StatusMapper) MapStatus(dbStatus string) string { if dbStatus == "" { return "" } - // Normalize to lowercase for lookup + // Normalise to lowercase for lookup normalized := strings.ToLower(strings.TrimSpace(dbStatus)) if mapped, exists := m.mapping[normalized]; exists { return mapped } - // If no mapping exists, return original value + // If no mapping exists, return the original value return dbStatus } diff --git a/pkg/models/all_urls_test.go b/pkg/models/all_urls_test.go index 07d341f..a0b327e 100644 --- a/pkg/models/all_urls_test.go +++ b/pkg/models/all_urls_test.go @@ -42,7 +42,7 @@ func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/models/common_test.go b/pkg/models/common_test.go index bb53427..788b182 100644 --- a/pkg/models/common_test.go +++ b/pkg/models/common_test.go @@ -79,7 +79,7 @@ func TestRunQueriesInParallel(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/models/component_status_test.go b/pkg/models/component_status_test.go index 0b766c8..0ba990a 100644 --- a/pkg/models/component_status_test.go +++ b/pkg/models/component_status_test.go @@ -45,7 +45,7 @@ func TestGetComponentStatusByPurlAndVersion(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -119,7 +119,7 @@ func TestGetComponentStatusByPurl(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -179,7 +179,7 @@ func TestGetProjectStatusByPurl(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/models/components_test.go b/pkg/models/components_test.go index 8ac959e..b2a27a1 100644 --- a/pkg/models/components_test.go +++ b/pkg/models/components_test.go @@ -44,7 +44,7 @@ func TestComponentsModel(t *testing.T) { if err != nil { t.Fatalf("failed to load SQL test data: %v", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/service/component_service_test.go b/pkg/service/component_service_test.go index 39f70e6..16eef0b 100644 --- a/pkg/service/component_service_test.go +++ b/pkg/service/component_service_test.go @@ -19,16 +19,17 @@ package service import ( "context" "encoding/json" + "reflect" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" common "github.com/scanoss/papi/api/commonv2" pb "github.com/scanoss/papi/api/componentsv2" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" - "reflect" myconfig "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/models" - "testing" ) func TestComponentServer_Echo(t *testing.T) { @@ -44,7 +45,7 @@ func TestComponentServer_Echo(t *testing.T) { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer models.CloseDB(db) - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -103,7 +104,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -184,7 +185,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -264,7 +265,7 @@ func TestComponentServer_GetComponentStatus(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } diff --git a/pkg/usecase/component_test.go b/pkg/usecase/component_test.go index 2cc9b4f..259dc2c 100644 --- a/pkg/usecase/component_test.go +++ b/pkg/usecase/component_test.go @@ -19,6 +19,8 @@ package usecase import ( "context" "fmt" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" @@ -27,7 +29,6 @@ import ( myconfig "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/dtos" "scanoss.com/components/pkg/models" - "testing" ) func TestComponentUseCase_SearchComponents(t *testing.T) { @@ -48,7 +49,7 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -106,7 +107,7 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -170,7 +171,7 @@ func TestComponentUseCase_GetComponentStatus(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -237,7 +238,7 @@ func TestComponentUseCase_GetComponentsStatus(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -304,7 +305,7 @@ func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when loading test data", err) } - myConfig, err := myconfig.NewServerConfig(nil, nil) + myConfig, err := myconfig.NewServerConfig(nil) if err != nil { t.Fatalf("failed to load Config: %v", err) } @@ -427,4 +428,3 @@ func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { }) } } - From 9c70e93f949cc881ba91c8bf621ee2149d7ede18 Mon Sep 17 00:00:00 2001 From: Sean Egan Date: Thu, 26 Mar 2026 18:03:49 +0000 Subject: [PATCH 12/13] updates to pass linter --- .golangci.yml | 18 ++-- cmd/server/main.go | 3 +- pkg/cmd/server.go | 36 ++++---- pkg/config/server_config.go | 2 +- pkg/config/server_config_integration_test.go | 10 ++- pkg/config/status_mapper.go | 18 ++-- pkg/config/status_mapper_test.go | 73 ++++++++-------- pkg/dtos/component_status_input.go | 38 +------- pkg/dtos/component_status_output.go | 92 ++------------------ pkg/dtos/component_version_input.go | 1 + pkg/dtos/component_version_input_test.go | 4 +- pkg/dtos/component_version_output.go | 4 +- pkg/dtos/component_version_output_test.go | 17 ++-- pkg/dtos/docs.go | 18 ++++ pkg/dtos/search_component_input.go | 1 + pkg/dtos/search_component_input_test.go | 3 +- pkg/dtos/search_component_output.go | 10 +-- pkg/dtos/search_component_output_test.go | 13 +-- pkg/errors/error.go | 1 + pkg/models/all_urls.go | 20 ++--- pkg/models/all_urls_test.go | 24 ++--- pkg/models/common.go | 26 +++--- pkg/models/common_test.go | 19 ++-- pkg/models/component_status.go | 12 +-- pkg/models/component_status_test.go | 12 ++- pkg/models/components.go | 6 +- pkg/models/components_test.go | 6 +- pkg/models/doc.go | 18 ++++ pkg/models/tests/bad_sql.sql | 2 +- pkg/protocol/rest/server.go | 4 +- pkg/service/component_service.go | 26 ++---- pkg/service/component_service_test.go | 22 +++-- pkg/service/component_support.go | 19 ++-- pkg/service/component_support_test.go | 11 ++- pkg/usecase/component.go | 29 +++--- pkg/usecase/component_test.go | 11 ++- 36 files changed, 279 insertions(+), 350 deletions(-) create mode 100644 pkg/dtos/docs.go create mode 100644 pkg/models/doc.go diff --git a/.golangci.yml b/.golangci.yml index a3a4c76..d63dd41 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -87,12 +87,12 @@ linters: exclusions: paths: - tests - rules: - - path: _test\.go - linters: - - gocognit - - govet - - cyclop - - godot - - funlen - - lll \ No newline at end of file + rules: + - path: _test\.go + linters: + - gocognit + - govet + - cyclop + - godot + - funlen + - lll \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 77547cb..bc5d51f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -20,10 +20,11 @@ package main import ( "fmt" "os" + "scanoss.com/components/pkg/cmd" ) -// main starts the gRPC Component Service +// main starts the gRPC Component Service. func main() { // Launch the Component Server Service if err := cmd.RunServer(); err != nil { diff --git a/pkg/cmd/server.go b/pkg/cmd/server.go index f3a1990..5d99425 100644 --- a/pkg/cmd/server.go +++ b/pkg/cmd/server.go @@ -27,6 +27,7 @@ import ( "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" + "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/scanoss/go-grpc-helper/pkg/files" gd "github.com/scanoss/go-grpc-helper/pkg/grpc/database" @@ -89,7 +90,7 @@ func getConfig() (*myconfig.ServerConfig, error) { return myConfig, err } -// RunServer runs the gRPC Component Server +// RunServer runs the gRPC Component Server. func RunServer() error { // Load command line options and config (logger is initialized inside getConfig) cfg, err := getConfig() @@ -107,14 +108,12 @@ func RunServer() error { if err != nil { return err } - // Set the default version from the embedded binary version if not overridden by config/env if len(cfg.App.Version) == 0 { cfg.App.Version = strings.TrimSpace(version) } zlog.S.Infof("Starting SCANOSS Component Service: %v", cfg.App.Version) - - // Setup database connection pool + // Set up the database connection pool db, err := gd.OpenDBConnection(cfg.Database.Dsn, cfg.Database.Driver, cfg.Database.User, cfg.Database.Passwd, cfg.Database.Host, cfg.Database.Schema, cfg.Database.SslMode) if err != nil { @@ -125,17 +124,8 @@ func RunServer() error { } defer gd.CloseDBConnection(db) // Log database version info - dbVersionModel := gomodels.NewDBVersionModel(db) - dbVersion, dbVersionErr := dbVersionModel.GetCurrentVersion(context.Background()) - if dbVersionErr != nil { - zlog.S.Warnf("Could not read db_version table: %v", dbVersionErr) - } else if len(dbVersion.SchemaVersion) > 0 { - zlog.S.Infof("Loaded decoration DB: package=%s, schema=%s, created_at=%s", - dbVersion.PackageName, dbVersion.SchemaVersion, dbVersion.CreatedAt) - } else { - zlog.S.Warn("db_version table is empty") - } - // Setup dynamic logging (if necessary) + logDBVersion(db) + // Set up dynamic logging (if necessary) zlog.SetupAppDynamicLogging(cfg.Logging.DynamicPort, cfg.Logging.DynamicLogging) // Register the component service v2API := service.NewComponentServer(db, cfg) @@ -155,3 +145,19 @@ func RunServer() error { // graceful shutdown return gs.WaitServerComplete(srv, server) } + +// logDBVersion logs the current version of the database. +func logDBVersion(db *sqlx.DB) { + // Log database version info + dbVersionModel := gomodels.NewDBVersionModel(db) + dbVersion, dbVersionErr := dbVersionModel.GetCurrentVersion(context.Background()) + switch { + case dbVersionErr != nil: + zlog.S.Warnf("Could not read db_version table: %v", dbVersionErr) + case len(dbVersion.SchemaVersion) > 0: + zlog.S.Infof("Loaded decoration DB: package=%s, schema=%s, created_at=%s", + dbVersion.PackageName, dbVersion.SchemaVersion, dbVersion.CreatedAt) + default: + zlog.S.Warn("db_version table is empty") + } +} diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 2beb181..cda7cbf 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -131,7 +131,7 @@ func setServerConfigDefaults(cfg *ServerConfig) { cfg.Telemetry.OltpExporter = "0.0.0.0:4317" // Default OTEL OLTP gRPC Exporter endpoint } -// InitStatusMapperConfig initialise the status mapper for mapping component statuses +// InitStatusMapperConfig initialise the status mapper for mapping component statuses. func (cfg *ServerConfig) InitStatusMapperConfig(s *zap.SugaredLogger) { cfg.statusMapper = NewStatusMapper(s, parseStatusMappingString(cfg.StatusMapping.Mapping)) } diff --git a/pkg/config/server_config_integration_test.go b/pkg/config/server_config_integration_test.go index 61fcb9b..64db7b9 100644 --- a/pkg/config/server_config_integration_test.go +++ b/pkg/config/server_config_integration_test.go @@ -17,6 +17,7 @@ package config import ( + "fmt" "os" "testing" @@ -37,11 +38,13 @@ func TestServerConfig_StatusMapping_FromEnv(t *testing.T) { if errEnv != nil { t.Fatalf("Could not set env variable: %v", errEnv) } - defer os.Unsetenv("STATUS_MAPPING") cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } + if ue := os.Unsetenv("STATUS_MAPPING"); ue != nil { + fmt.Printf("Warning: Problem running Unsetenv: %v\n", ue) + } // Allowing the GetStatusMapper to load the config mapper := cfg.GetStatusMapper() if mapper == nil { @@ -110,11 +113,14 @@ func TestServerConfig_StatusMapping_WithProvidedLogger(t *testing.T) { if errEnv != nil { t.Fatalf("Could not set env variable: %v", errEnv) } - defer os.Unsetenv("STATUS_MAPPING") cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("Failed to load config: %v", err) } + if ue := os.Unsetenv("STATUS_MAPPING"); ue != nil { + fmt.Printf("Warning: Problem running Unsetenv: %v\n", ue) + } + cfg.InitStatusMapperConfig(zlog.S) mapper := cfg.GetStatusMapper() if mapper == nil { diff --git a/pkg/config/status_mapper.go b/pkg/config/status_mapper.go index 1ad9139..efd222a 100644 --- a/pkg/config/status_mapper.go +++ b/pkg/config/status_mapper.go @@ -23,7 +23,7 @@ import ( "go.uber.org/zap" ) -// StatusMapper handles mapping of database statuses to classified statuses +// StatusMapper handles mapping of database statuses to classified statuses. type StatusMapper struct { mapping map[string]string s *zap.SugaredLogger @@ -56,7 +56,7 @@ func NewStatusMapper(s *zap.SugaredLogger, mappingConfig interface{}) *StatusMap return mapper } -// parseMappingConfig parses the mapping configuration from various formats +// parseMappingConfig parses the mapping configuration from various formats. func parseMappingConfig(s *zap.SugaredLogger, mappingConfig interface{}) map[string]string { switch v := mappingConfig.(type) { case string: @@ -76,7 +76,7 @@ func parseMappingConfig(s *zap.SugaredLogger, mappingConfig interface{}) map[str } } -// parseJSONString parses a JSON string into a map +// parseJSONString parses a JSON string into a map. func parseJSONString(s *zap.SugaredLogger, jsonStr string) map[string]string { if len(strings.TrimSpace(jsonStr)) == 0 { return nil @@ -92,23 +92,21 @@ func parseJSONString(s *zap.SugaredLogger, jsonStr string) map[string]string { return result } -// convertInterfaceMap converts map[string]interface{} to map[string]string +// convertInterfaceMap converts map[string]interface{} to map[string]string. func convertInterfaceMap(s *zap.SugaredLogger, m map[string]interface{}) map[string]string { result := make(map[string]string, len(m)) for key, value := range m { if strValue, ok := value.(string); ok { result[key] = strValue - } else { - if s != nil { - s.Warnf("Skipping non-string value for key %q: %v (type: %T)", key, value, value) - } + } else if s != nil { + s.Warnf("Skipping non-string value for key %q: %v (type: %T)", key, value, value) } } return result } // MapStatus maps a database status to its classified status -// Returns the mapped status, or the original if no mapping exists +// Returns the mapped status, or the original if no mapping exists. func (m *StatusMapper) MapStatus(dbStatus string) string { if dbStatus == "" { return "" @@ -122,7 +120,7 @@ func (m *StatusMapper) MapStatus(dbStatus string) string { return dbStatus } -// getDefaultStatusMapping returns the default status classification mapping +// getDefaultStatusMapping returns the default status classification mapping. func getDefaultStatusMapping() map[string]string { return map[string]string{ "active": "active", diff --git a/pkg/config/status_mapper_test.go b/pkg/config/status_mapper_test.go index 9323efa..cb7508f 100644 --- a/pkg/config/status_mapper_test.go +++ b/pkg/config/status_mapper_test.go @@ -22,6 +22,9 @@ import ( zlog "github.com/scanoss/zap-logging-helper/pkg/logger" ) +const removedStatus = "removed" +const activeStatus = "active" + func TestStatusMapper_MapStatus_DefaultMappings(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -38,15 +41,15 @@ func TestStatusMapper_MapStatus_DefaultMappings(t *testing.T) { input string expected string }{ - {"active maps to active", "active", "active"}, - {"unlisted maps to removed", "unlisted", "removed"}, - {"yanked maps to removed", "yanked", "removed"}, + {"active maps to active", activeStatus, activeStatus}, + {"unlisted maps to removed", "unlisted", removedStatus}, + {"yanked maps to removed", "yanked", removedStatus}, {"deleted maps to deleted", "deleted", "deleted"}, {"deprecated maps to deprecated", "deprecated", "deprecated"}, - {"unpublished maps to removed", "unpublished", "removed"}, + {"unpublished maps to removed", "unpublished", removedStatus}, {"archived maps to deprecated", "archived", "deprecated"}, {"ACTIVE (uppercase) maps to active", "ACTIVE", "active"}, - {"Unlisted (mixed case) maps to removed", "Unlisted", "removed"}, + {"Unlisted (mixed case) maps to removed", "Unlisted", removedStatus}, {"unknown status returns original", "unknown-status", "unknown-status"}, {"empty string returns empty", "", ""}, {"whitespace status returns original", " some status ", " some status "}, @@ -81,7 +84,7 @@ func TestStatusMapper_MapStatus_CustomMappings(t *testing.T) { }{ {"custom unlisted mapping", "unlisted", "custom-removed"}, {"custom new-status mapping", "new-status", "custom-value"}, - {"custom active override", "active", "still-active"}, + {"custom active override", activeStatus, "still-active"}, {"default yanked still works", "yanked", "removed"}, {"default deleted still works", "deleted", "deleted"}, {"unknown status returns original", "completely-unknown", "completely-unknown"}, @@ -111,13 +114,13 @@ func TestStatusMapper_MapStatus_InvalidJSON(t *testing.T) { // Should use defaults when JSON is invalid result := mapper.MapStatus("unlisted") - if result != "removed" { - t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, "removed") + if result != removedStatus { + t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, removedStatus) } - result = mapper.MapStatus("active") - if result != "active" { - t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, "active") + result = mapper.MapStatus(activeStatus) + if result != activeStatus { + t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, activeStatus) } } @@ -137,8 +140,8 @@ func TestStatusMapper_MapStatus_EmptyJSON(t *testing.T) { // Should use defaults result := mapper.MapStatus("unlisted") - if result != "removed" { - t.Errorf("MapStatus with empty JSON %q should use defaults, got %q, expected %q", emptyJSON, result, "removed") + if result != removedStatus { + t.Errorf("MapStatus with empty JSON %q should use defaults, got %q, expected %q", emptyJSON, result, removedStatus) } } } @@ -158,14 +161,14 @@ func TestStatusMapper_MapStatus_CaseSensitivity(t *testing.T) { input string expected string }{ - {"active", "active"}, - {"ACTIVE", "active"}, - {"Active", "active"}, - {"AcTiVe", "active"}, - {"unlisted", "removed"}, - {"UNLISTED", "removed"}, - {"Unlisted", "removed"}, - {"UnLiStEd", "removed"}, + {activeStatus, activeStatus}, + {"ACTIVE", activeStatus}, + {"Active", activeStatus}, + {"AcTiVe", activeStatus}, + {"unlisted", removedStatus}, + {"UNLISTED", removedStatus}, + {"Unlisted", removedStatus}, + {"UnLiStEd", removedStatus}, } for _, tc := range testCases { @@ -180,12 +183,12 @@ func TestGetDefaultStatusMapping(t *testing.T) { defaults := getDefaultStatusMapping() expectedMappings := map[string]string{ - "active": "active", - "unlisted": "removed", - "yanked": "removed", + activeStatus: activeStatus, + "unlisted": removedStatus, + "yanked": removedStatus, "deleted": "deleted", "deprecated": "deprecated", - "unpublished": "removed", + "unpublished": removedStatus, "archived": "deprecated", } @@ -214,7 +217,7 @@ func TestStatusMapper_MapStatus_MapFormat(t *testing.T) { customMapping := map[string]interface{}{ "unlisted": "custom-removed", "new-status": "custom-value", - "active": "still-active", + activeStatus: "still-active", } mapper := NewStatusMapper(s, customMapping) @@ -225,8 +228,8 @@ func TestStatusMapper_MapStatus_MapFormat(t *testing.T) { }{ {"map format: custom unlisted mapping", "unlisted", "custom-removed"}, {"map format: custom new-status mapping", "new-status", "custom-value"}, - {"map format: custom active override", "active", "still-active"}, - {"map format: default yanked still works", "yanked", "removed"}, + {"map format: custom active override", activeStatus, "still-active"}, + {"map format: default yanked still works", "yanked", removedStatus}, {"map format: default deleted still works", "deleted", "deleted"}, {"map format: unknown status returns original", "completely-unknown", "completely-unknown"}, } @@ -253,8 +256,8 @@ func TestStatusMapper_NilConfig(t *testing.T) { mapper := NewStatusMapper(s, nil) result := mapper.MapStatus("unlisted") - if result != "removed" { - t.Errorf("MapStatus with nil config should use defaults, got %q, expected %q", result, "removed") + if result != removedStatus { + t.Errorf("MapStatus with nil config should use defaults, got %q, expected %q", result, removedStatus) } } @@ -268,8 +271,8 @@ func TestStatusMapper_MapWithNonStringValue(t *testing.T) { // Create mapper with map containing non-string values customMapping := map[string]interface{}{ - "unlisted": "custom-removed", - "active": 123, // Invalid: not a string + "unlisted": "custom-removed", + activeStatus: 123, // Invalid: not a string } mapper := NewStatusMapper(s, customMapping) @@ -280,8 +283,8 @@ func TestStatusMapper_MapWithNonStringValue(t *testing.T) { } // active should use default (non-string value was skipped) - result = mapper.MapStatus("active") - if result != "active" { - t.Errorf("MapStatus('active') with non-string value should use default, got %q, expected %q", result, "active") + result = mapper.MapStatus(activeStatus) + if result != activeStatus { + t.Errorf("MapStatus('active') with non-string value should use default, got %q, expected %q", result, activeStatus) } } diff --git a/pkg/dtos/component_status_input.go b/pkg/dtos/component_status_input.go index bdb4949..59e5af7 100644 --- a/pkg/dtos/component_status_input.go +++ b/pkg/dtos/component_status_input.go @@ -8,34 +8,17 @@ import ( "go.uber.org/zap" ) -// ComponentStatusInput represents a single component status request +// ComponentStatusInput represents a single component status request. type ComponentStatusInput struct { Purl string `json:"purl"` Requirement string `json:"requirement,omitempty"` } -// ComponentsStatusInput represents a request for multiple component statuses +// ComponentsStatusInput represents a request for multiple component statuses. type ComponentsStatusInput struct { Components []ComponentStatusInput `json:"components"` } -// ExportComponentStatusInput marshals a ComponentStatusInput struct to JSON bytes. -// -// Parameters: -// - s: Sugared logger for error logging -// - output: ComponentStatusInput struct to be marshaled -// -// Returns: -// - JSON byte array representation of the input, or error if marshaling fails -func ExportComponentStatusInput(s *zap.SugaredLogger, output ComponentStatusInput) ([]byte, error) { - data, err := json.Marshal(output) - if err != nil { - s.Errorf("Parse failure: %v", err) - return nil, errors.New("failed to produce JSON") - } - return data, nil -} - // ParseComponentStatusInput unmarshals JSON bytes into a ComponentStatusInput struct. // // Parameters: @@ -57,23 +40,6 @@ func ParseComponentStatusInput(s *zap.SugaredLogger, input []byte) (ComponentSta return data, nil } -// ExportComponentsStatusInput marshals a ComponentsStatusInput struct (containing multiple components) to JSON bytes. -// -// Parameters: -// - s: Sugared logger for error logging -// - output: ComponentsStatusInput struct containing an array of component status requests -// -// Returns: -// - JSON byte array representation of the input, or error if marshaling fails -func ExportComponentsStatusInput(s *zap.SugaredLogger, output ComponentsStatusInput) ([]byte, error) { - data, err := json.Marshal(output) - if err != nil { - s.Errorf("Parse failure: %v", err) - return nil, errors.New("failed to produce JSON") - } - return data, nil -} - // ParseComponentsStatusInput unmarshals JSON bytes into a ComponentsStatusInput struct. // Used for parsing batch component status requests containing multiple components. // diff --git a/pkg/dtos/component_status_output.go b/pkg/dtos/component_status_output.go index 7ad3367..ff06152 100644 --- a/pkg/dtos/component_status_output.go +++ b/pkg/dtos/component_status_output.go @@ -1,15 +1,10 @@ package dtos import ( - "encoding/json" - "errors" - "fmt" - "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" - "go.uber.org/zap" ) -// ComponentStatusOutput represents the status information for a single component +// ComponentStatusOutput represents the status information for a single component. type ComponentStatusOutput struct { Purl string `json:"purl"` Name string `json:"name"` @@ -18,7 +13,7 @@ type ComponentStatusOutput struct { ComponentStatus *ComponentStatusInfo `json:"component_status,omitempty"` } -// VersionStatusOutput represents the status of a specific version +// VersionStatusOutput represents the status of a specific version. type VersionStatusOutput struct { Version string `json:"version"` Status string `json:"status"` @@ -29,7 +24,7 @@ type VersionStatusOutput struct { ErrorCode *domain.StatusCode `json:"error_code,omitempty"` } -// ComponentStatusInfo represents the status of a component (ignoring version) +// ComponentStatusInfo represents the status of a component (ignoring version). type ComponentStatusInfo struct { Status string `json:"status"` RepositoryStatus string `json:"repository_status,omitempty"` @@ -40,90 +35,13 @@ type ComponentStatusInfo struct { ErrorCode *domain.StatusCode `json:"error_code,omitempty"` } -// ComponentsStatusOutput represents the status information for multiple components +// ComponentsStatusOutput represents the status information for multiple components. type ComponentsStatusOutput struct { Components []ComponentStatusOutput `json:"components"` } -// ExportComponentStatusOutput marshals a ComponentStatusOutput struct to JSON bytes. -// -// Parameters: -// - s: Sugared logger for error logging -// - output: ComponentStatusOutput struct to be marshaled -// -// Returns: -// - JSON byte array representation of the output, or error if marshaling fails -func ExportComponentStatusOutput(s *zap.SugaredLogger, output ComponentStatusOutput) ([]byte, error) { - data, err := json.Marshal(output) - if err != nil { - s.Errorf("Marshal failure: %v", err) - return nil, errors.New("failed to produce JSON") - } - return data, nil -} - -// ParseComponentStatusOutput unmarshals JSON bytes into a ComponentStatusOutput struct. -// -// Parameters: -// - s: Sugared logger for error logging -// - output: JSON byte array to be unmarshaled -// -// Returns: -// - ComponentStatusOutput struct populated from JSON, or error if unmarshaling fails or input is empty -func ParseComponentStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentStatusOutput, error) { - if len(output) == 0 { - return ComponentStatusOutput{}, errors.New("no data supplied to parse") - } - var data ComponentStatusOutput - err := json.Unmarshal(output, &data) - if err != nil { - s.Errorf("Parse failure: %v", err) - return ComponentStatusOutput{}, fmt.Errorf("failed to parse data: %v", err) - } - return data, nil -} - -// ExportComponentsStatusOutput marshals a ComponentsStatusOutput struct (containing multiple components) to JSON bytes. -// -// Parameters: -// - s: Sugared logger for error logging -// - output: ComponentsStatusOutput struct containing an array of component statuses -// -// Returns: -// - JSON byte array representation of the output, or error if marshaling fails -func ExportComponentsStatusOutput(s *zap.SugaredLogger, output ComponentsStatusOutput) ([]byte, error) { - data, err := json.Marshal(output) - if err != nil { - s.Errorf("Marshall failure: %v", err) - return nil, errors.New("failed to produce JSON") - } - return data, nil -} - -// ParseComponentsStatusOutput unmarshals JSON bytes into a ComponentsStatusOutput struct. -// Used for parsing batch component status responses containing multiple components. -// -// Parameters: -// - s: Sugared logger for error logging -// - output: JSON byte array to be unmarshaled -// -// Returns: -// - ComponentsStatusOutput struct with array of component statuses, or error if unmarshaling fails or input is empty -func ParseComponentsStatusOutput(s *zap.SugaredLogger, output []byte) (ComponentsStatusOutput, error) { - if len(output) == 0 { - return ComponentsStatusOutput{}, errors.New("no data supplied to parse") - } - var data ComponentsStatusOutput - err := json.Unmarshal(output, &data) - if err != nil { - s.Errorf("Parse failure: %v", err) - return ComponentsStatusOutput{}, fmt.Errorf("failed to parse data: %v", err) - } - return data, nil -} - // StringPtr returns a pointer to the provided string value -// This is useful for optional string fields that require pointers +// This is useful for optional string fields that require pointers. func StringPtr(s string) *string { return &s } diff --git a/pkg/dtos/component_version_input.go b/pkg/dtos/component_version_input.go index 2bd1b82..07b071d 100644 --- a/pkg/dtos/component_version_input.go +++ b/pkg/dtos/component_version_input.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) diff --git a/pkg/dtos/component_version_input_test.go b/pkg/dtos/component_version_input_test.go index 49185d2..d856390 100644 --- a/pkg/dtos/component_version_input_test.go +++ b/pkg/dtos/component_version_input_test.go @@ -3,10 +3,11 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) func TestParseComponentVersionsInput(t *testing.T) { @@ -96,5 +97,4 @@ func TestExportComponentVersionsInput(t *testing.T) { t.Errorf("Failed to export component version input: %v\n", err) } fmt.Printf("Converting empty component version input json to bytes: %v\n", bytes) - } diff --git a/pkg/dtos/component_version_output.go b/pkg/dtos/component_version_output.go index 0b69d88..13a2835 100644 --- a/pkg/dtos/component_version_output.go +++ b/pkg/dtos/component_version_output.go @@ -17,7 +17,7 @@ type ComponentOutput struct { Component string `json:"component"` // Deprecated. Component and name fields will contain the same data until // the component field is removed Purl string `json:"purl"` - Url string `json:"url"` + URL string `json:"url"` Versions []ComponentVersion `json:"versions"` } @@ -29,7 +29,7 @@ type ComponentVersion struct { type ComponentLicense struct { Name string `json:"name"` - SpdxId string `json:"spdx_id"` + SpdxID string `json:"spdx_id"` IsSpdx bool `json:"is_spdx_approved"` } diff --git a/pkg/dtos/component_version_output_test.go b/pkg/dtos/component_version_output_test.go index 84dc123..2d05c70 100644 --- a/pkg/dtos/component_version_output_test.go +++ b/pkg/dtos/component_version_output_test.go @@ -10,6 +10,7 @@ import ( zlog "github.com/scanoss/zap-logging-helper/pkg/logger" ) +//goland:noinspection DuplicatedCode func TestParseComponentVersionsOutput(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -36,14 +37,14 @@ func TestParseComponentVersionsOutput(t *testing.T) { want: ComponentVersionsOutput{Component: ComponentOutput{ Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", Versions: []ComponentVersion{ { Version: "1.8.3", Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -53,7 +54,7 @@ func TestParseComponentVersionsOutput(t *testing.T) { Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -105,14 +106,14 @@ func TestExportComponentVersionsOutput(t *testing.T) { fullComponent := ComponentVersionsOutput{Component: ComponentOutput{ Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", Versions: []ComponentVersion{ { Version: "1.8.3", Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -122,7 +123,7 @@ func TestExportComponentVersionsOutput(t *testing.T) { Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -134,11 +135,11 @@ func TestExportComponentVersionsOutput(t *testing.T) { if err != nil { t.Errorf("dtos.ExportComponentVersionsOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) + fmt.Println("Exported output data: ", string(data)) data, err = ExportComponentVersionsOutput(s, ComponentVersionsOutput{}) if err != nil { t.Errorf("dtos.ExportComponentVersionsOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) + fmt.Println("Exported output data: ", string(data)) } diff --git a/pkg/dtos/docs.go b/pkg/dtos/docs.go new file mode 100644 index 0000000..c50677c --- /dev/null +++ b/pkg/dtos/docs.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Package dtos contains data transfer objects (DTOs) for the Component service. +package dtos diff --git a/pkg/dtos/search_component_input.go b/pkg/dtos/search_component_input.go index 3d8b6ff..86724c0 100644 --- a/pkg/dtos/search_component_input.go +++ b/pkg/dtos/search_component_input.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) diff --git a/pkg/dtos/search_component_input_test.go b/pkg/dtos/search_component_input_test.go index 5d1d98a..e07a321 100644 --- a/pkg/dtos/search_component_input_test.go +++ b/pkg/dtos/search_component_input_test.go @@ -3,10 +3,11 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) func TestParseComponentSearchInput(t *testing.T) { diff --git a/pkg/dtos/search_component_output.go b/pkg/dtos/search_component_output.go index b06526b..c21f22e 100644 --- a/pkg/dtos/search_component_output.go +++ b/pkg/dtos/search_component_output.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) @@ -12,11 +13,10 @@ type ComponentsSearchOutput struct { } type ComponentSearchOutput struct { - Name string `json:"name"` - Component string `json:"component"` // Deprecated. Component and name fields will contain the same data until - // the component field is removed - Purl string `json:"purl"` - Url string `json:"url"` + Name string `json:"name"` // Deprecated. Component and name fields will contain the same data until + Component string `json:"component"` // the component field is removed + Purl string `json:"purl"` + URL string `json:"url"` } func ExportComponentSearchOutput(s *zap.SugaredLogger, output ComponentsSearchOutput) ([]byte, error) { diff --git a/pkg/dtos/search_component_output_test.go b/pkg/dtos/search_component_output_test.go index fd5d259..311141f 100644 --- a/pkg/dtos/search_component_output_test.go +++ b/pkg/dtos/search_component_output_test.go @@ -3,12 +3,14 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) +//goland:noinspection DuplicatedCode func TestParseComponentSearchOutput(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -28,7 +30,7 @@ func TestParseComponentSearchOutput(t *testing.T) { { Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", }, }}, }, @@ -86,7 +88,7 @@ func TestExportComponentSearchOutput(t *testing.T) { { Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", }, }} @@ -94,12 +96,11 @@ func TestExportComponentSearchOutput(t *testing.T) { if err != nil { t.Errorf("ExportComponentSearchOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) + fmt.Println("Exported output data: ", string(data)) data, err = ExportComponentSearchOutput(s, ComponentsSearchOutput{}) if err != nil { t.Errorf("ExportComponentSearchOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) - + fmt.Println("Exported output data: ", string(data)) } diff --git a/pkg/errors/error.go b/pkg/errors/error.go index 933478e..a9bf0b6 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +// Package errors contains error handling logic for the Component service. package errors import ( diff --git a/pkg/models/all_urls.go b/pkg/models/all_urls.go index 67eacf3..6029720 100644 --- a/pkg/models/all_urls.go +++ b/pkg/models/all_urls.go @@ -27,29 +27,29 @@ import ( "go.uber.org/zap" ) -type AllUrlsModel struct { +type AllURLsModel struct { ctx context.Context s *zap.SugaredLogger q *database.DBQueryContext } -type AllUrl struct { +type AllURL struct { Version string `db:"version"` Component string `db:"component"` License string `db:"license"` - LicenseId string `db:"license_id"` + LicenseID string `db:"license_id"` IsSpdx bool `db:"is_spdx"` PurlName string `db:"purl_name"` - MineId int32 `db:"mine_id"` + MineID int32 `db:"mine_id"` Date sql.NullString `db:"date"` - Url string `db:"-"` + URL string `db:"-"` } -func NewAllUrlModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext) *AllUrlsModel { - return &AllUrlsModel{ctx: ctx, s: s, q: q} +func NewAllURLModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext) *AllURLsModel { + return &AllURLsModel{ctx: ctx, s: s, q: q} } -func (m *AllUrlsModel) GetUrlsByPurlString(purlString string, limit int) ([]AllUrl, error) { +func (m *AllURLsModel) GetUrlsByPurlString(purlString string, limit int) ([]AllURL, error) { if len(purlString) == 0 { m.s.Errorf("Please specify a valid Purl String to query") return nil, errors.New("please specify a valid Purl String to query") @@ -65,7 +65,7 @@ func (m *AllUrlsModel) GetUrlsByPurlString(purlString string, limit int) ([]AllU return m.GetUrlsByPurlNameType(purlName, purl.Type, limit) } -func (m *AllUrlsModel) GetUrlsByPurlNameType(purlName, purlType string, limit int) ([]AllUrl, error) { +func (m *AllURLsModel) GetUrlsByPurlNameType(purlName, purlType string, limit int) ([]AllURL, error) { if len(purlName) == 0 { m.s.Errorf("Please specify a valid Purl Name to query") return nil, errors.New("please specify a valid Purl Name to query") @@ -79,7 +79,7 @@ func (m *AllUrlsModel) GetUrlsByPurlNameType(purlName, purlType string, limit in limit = defaultMaxVersionLimit } - var allUrls []AllUrl + var allUrls []AllURL err := m.q.SelectContext(m.ctx, &allUrls, ` SELECT DISTINCT diff --git a/pkg/models/all_urls_test.go b/pkg/models/all_urls_test.go index a0b327e..bed373f 100644 --- a/pkg/models/all_urls_test.go +++ b/pkg/models/all_urls_test.go @@ -27,8 +27,8 @@ import ( myconfig "scanoss.com/components/pkg/config" ) -// setupTest initialises all necessary components for testing -func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { +// setupTest initialises all necessary components for testing. +func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllURLsModel) { err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) @@ -48,17 +48,17 @@ func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { } myConfig.Database.Trace = true - return db, conn, NewAllUrlModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + return db, conn, NewAllURLModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) } -// cleanup handles proper resource cleanup +// cleanup handles proper resource cleanup. func cleanup(db *sqlx.DB, conn *sqlx.Conn) { CloseConn(conn) CloseDB(db) zlog.SyncZap() } -// TestGetUrlsByPurlNameType tests the GetUrlsByPurlNameType function +// TestGetUrlsByPurlNameType tests the GetUrlsByPurlNameType function. func TestGetUrlsByPurlNameType(t *testing.T) { db, conn, allUrlsModel := setupTest(t) defer cleanup(db, conn) @@ -70,7 +70,7 @@ func TestGetUrlsByPurlNameType(t *testing.T) { limit int wantErr bool wantEmpty bool - validate func(t *testing.T, urls []AllUrl) + validate func(t *testing.T, urls []AllURL) }{ { name: "valid url search", @@ -79,7 +79,7 @@ func TestGetUrlsByPurlNameType(t *testing.T) { limit: -1, wantErr: false, wantEmpty: false, - validate: func(t *testing.T, urls []AllUrl) { + validate: func(t *testing.T, urls []AllURL) { if urls[0].PurlName != "tablestyle" { t.Errorf("expected purlName 'tablestyle', got %s", urls[0].PurlName) } @@ -92,9 +92,9 @@ func TestGetUrlsByPurlNameType(t *testing.T) { limit: 200, wantErr: false, wantEmpty: false, - validate: func(t *testing.T, urls []AllUrl) { + validate: func(t *testing.T, urls []AllURL) { // Filter URLs for specific version - var matchedUrls []AllUrl + var matchedUrls []AllURL for _, url := range urls { if url.PurlName == "grpcio" && url.Version == "1.12.1" { matchedUrls = append(matchedUrls, url) @@ -182,7 +182,7 @@ func TestGetUrlsByPurlNameType(t *testing.T) { } } -// TestGetUrlsByPurlString tests the GetUrlsByPurlString function +// TestGetUrlsByPurlString tests the GetUrlsByPurlString function. func TestGetUrlsByPurlString(t *testing.T) { db, conn, allUrlsModel := setupTest(t) defer cleanup(db, conn) @@ -193,7 +193,7 @@ func TestGetUrlsByPurlString(t *testing.T) { limit int wantErr bool wantEmpty bool - validate func(t *testing.T, urls []AllUrl) + validate func(t *testing.T, urls []AllURL) }{ { name: "valid purl", @@ -201,7 +201,7 @@ func TestGetUrlsByPurlString(t *testing.T) { limit: -1, wantErr: false, wantEmpty: false, - validate: func(t *testing.T, urls []AllUrl) { + validate: func(t *testing.T, urls []AllURL) { if urls[0].PurlName != "tablestyle" { t.Errorf("expected purlName 'tablestyle', got %s", urls[0].PurlName) } diff --git a/pkg/models/common.go b/pkg/models/common.go index ddd2260..b277b9e 100644 --- a/pkg/models/common.go +++ b/pkg/models/common.go @@ -21,16 +21,16 @@ package models import ( "context" "fmt" - "github.com/scanoss/go-grpc-helper/pkg/grpc/database" "os" "testing" "github.com/jmoiron/sqlx" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" ) -// loadSqlData Load the specified SQL files into the supplied DB -func loadSqlData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, filename string) error { +// loadSQLData Load the specified SQL files into the supplied DB. +func loadSQLData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, filename string) error { fmt.Printf("Loading test data file: %v\n", filename) file, err := os.ReadFile(filename) if err != nil { @@ -47,17 +47,17 @@ func loadSqlData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, filename str return nil } -// LoadTestSQLData loads all the required test SQL files +// LoadTestSQLData loads all the required test SQL files. func LoadTestSQLData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn) error { files := []string{"../models/tests/mines.sql", "../models/tests/all_urls.sql", "../models/tests/projects.sql", "../models/tests/licenses.sql", "../models/tests/versions.sql"} - return loadTestSqlDataFiles(db, ctx, conn, files) + return loadTestSQLDataFiles(db, ctx, conn, files) } -// loadTestSqlDataFiles loads a list of test SQL files -func loadTestSqlDataFiles(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, files []string) error { +// loadTestSQLDataFiles loads a list of test SQL files. +func loadTestSQLDataFiles(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, files []string) error { for _, file := range files { - err := loadSqlData(db, ctx, conn, file) + err := loadSQLData(db, ctx, conn, file) if err != nil { return err } @@ -109,13 +109,13 @@ type QueryJob struct { } type job struct { - jobId int + jobID int query string args []any } type result[T any] struct { - jobId int + jobID int query string err error dest []T @@ -126,7 +126,7 @@ func workerQuery[T any](q *database.DBQueryContext, ctx context.Context, jobs ch for j := range jobs { err := q.SelectContext(ctx, &structResults, j.query, j.args...) results <- result[T]{ - jobId: j.jobId, + jobID: j.jobID, query: j.query, err: err, dest: structResults, @@ -145,7 +145,7 @@ func RunQueries[T any](q *database.DBQueryContext, ctx context.Context, queryJob for i, queryJob := range queryJobs { jobChan <- job{ - jobId: i, + jobID: i, query: queryJob.Query, args: queryJob.Args, } @@ -156,7 +156,7 @@ func RunQueries[T any](q *database.DBQueryContext, ctx context.Context, queryJob for i := 0; i < numJobs; i++ { res := <-resultChan if res.err == nil { - resMap[res.jobId] = res.dest + resMap[res.jobID] = res.dest } else { return []T{}, res.err } diff --git a/pkg/models/common_test.go b/pkg/models/common_test.go index 788b182..fe92c1f 100644 --- a/pkg/models/common_test.go +++ b/pkg/models/common_test.go @@ -42,7 +42,7 @@ func TestDbLoad(t *testing.T) { t.Errorf("an error '%s' was not expected when opening a sugared logger", err) } defer CloseDB(db) - err = loadSqlData(db, nil, nil, "./tests/mines.sql") + err = loadSQLData(db, nil, nil, "./tests/mines.sql") if err != nil { t.Errorf("failed to load SQL test data: %v", err) } @@ -50,22 +50,21 @@ func TestDbLoad(t *testing.T) { if err != nil { t.Errorf("failed to load SQL test data: %v", err) } - err = loadSqlData(db, nil, nil, "./tests/does-not-exist.sql") + err = loadSQLData(db, nil, nil, "./tests/does-not-exist.sql") if err == nil { t.Errorf("did not fail to load SQL test data") } - err = loadTestSqlDataFiles(db, nil, nil, []string{"./tests/does-not-exist.sql"}) + err = loadTestSQLDataFiles(db, nil, nil, []string{"./tests/does-not-exist.sql"}) if err == nil { t.Errorf("did not fail to load SQL test data") } - err = loadSqlData(db, nil, nil, "./tests/bad_sql.sql") + err = loadSQLData(db, nil, nil, "./tests/bad_sql.sql") if err == nil { t.Errorf("did not fail to load SQL test data") } } func TestRunQueriesInParallel(t *testing.T) { - err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) @@ -100,11 +99,9 @@ func TestRunQueriesInParallel(t *testing.T) { t.Errorf("Error running multiple queries %v", err) } fmt.Printf("Result of running queries %v:\n%v\n ", queryJobs, res) - } func TestRemoveDuplicates(t *testing.T) { - err := zlog.NewSugaredDevLogger() if err != nil { t.Errorf("an error '%s' was not expected when opening a sugared logger", err) @@ -115,28 +112,28 @@ func TestRemoveDuplicates(t *testing.T) { Component: "hyx-decrypt", PurlType: "npm", PurlName: "hyx-decrypt", - Url: "", + URL: "", } comp1 := Component{ Component: "scanner", PurlType: "npm", PurlName: "scanner", - Url: "https://www.npmjs.com/package/scanner", + URL: "https://www.npmjs.com/package/scanner", } comp1Similar := Component{ Component: "scanner", PurlType: "npm", PurlName: "scanner", - Url: "www.npmjs.com/package/scanner", + URL: "www.npmjs.com/package/scanner", } comp2 := Component{ Component: "graph", PurlType: "npm", PurlName: "graph", - Url: "https://www.npmjs.com/package/graph", + URL: "https://www.npmjs.com/package/graph", } testTable := []struct { diff --git a/pkg/models/component_status.go b/pkg/models/component_status.go index 85f4636..c655970 100644 --- a/pkg/models/component_status.go +++ b/pkg/models/component_status.go @@ -33,7 +33,7 @@ type ComponentStatusModel struct { q *database.DBQueryContext } -// ComponentVersionStatus represents the status information for a specific version +// ComponentVersionStatus represents the status information for a specific version. type ComponentVersionStatus struct { PurlName string `db:"purl_name"` Version string `db:"version"` @@ -42,7 +42,7 @@ type ComponentVersionStatus struct { VersionStatusChangeDate sql.NullString `db:"version_status_change_date"` } -// ComponentProjectStatus represents the status information for a component (ignoring version) +// ComponentProjectStatus represents the status information for a component (ignoring version). type ComponentProjectStatus struct { PurlName string `db:"purl_name"` Component string `db:"component"` @@ -52,7 +52,7 @@ type ComponentProjectStatus struct { StatusChangeDate sql.NullString `db:"status_change_date"` } -// ComponentFullStatus combines version and project status information +// ComponentFullStatus combines version and project status information. type ComponentFullStatus struct { ComponentVersionStatus ComponentProjectStatus @@ -62,7 +62,7 @@ func NewComponentStatusModel(ctx context.Context, s *zap.SugaredLogger, q *datab return &ComponentStatusModel{ctx: ctx, s: s, q: q} } -// GetComponentStatusByPurlAndVersion gets status information for a specific component version +// GetComponentStatusByPurlAndVersion gets status information for a specific component version. func (m *ComponentStatusModel) GetComponentStatusByPurlAndVersion(purlString, version string) (*ComponentVersionStatus, error) { if len(purlString) == 0 { m.s.Errorf("Please specify a valid Purl String to query") @@ -101,7 +101,7 @@ func (m *ComponentStatusModel) GetComponentStatusByPurlAndVersion(purlString, ve return &status, nil } -// GetComponentStatusByPurl gets status information for the latest version of a component +// GetComponentStatusByPurl gets status information for the latest version of a component. func (m *ComponentStatusModel) GetComponentStatusByPurl(purlString string) (*ComponentProjectStatus, error) { if len(purlString) == 0 { m.s.Errorf("Please specify a valid Purl String to query") @@ -147,7 +147,7 @@ func (m *ComponentStatusModel) GetComponentStatusByPurl(purlString string) (*Com return &status, nil } -// GetProjectStatusByPurl gets only the project-level status (no version information) +// GetProjectStatusByPurl gets only the project-level status (no version information). func (m *ComponentStatusModel) GetProjectStatusByPurl(purlString string) (*ComponentProjectStatus, error) { if len(purlString) == 0 { m.s.Errorf("Please specify a valid Purl String to query") diff --git a/pkg/models/component_status_test.go b/pkg/models/component_status_test.go index 0ba990a..553f850 100644 --- a/pkg/models/component_status_test.go +++ b/pkg/models/component_status_test.go @@ -28,7 +28,9 @@ import ( myconfig "scanoss.com/components/pkg/config" ) -// TestGetComponentStatusByPurlAndVersion tests retrieving status for a specific component version +// TestGetComponentStatusByPurlAndVersion tests retrieving status for a specific component version. +// +//goland:noinspection DuplicatedCode func TestGetComponentStatusByPurlAndVersion(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -102,7 +104,9 @@ func TestGetComponentStatusByPurlAndVersion(t *testing.T) { } } -// TestGetComponentStatusByPurl tests retrieving status for a component (without version) +// TestGetComponentStatusByPurl tests retrieving status for a component (without version). +// +//goland:noinspection DuplicatedCode func TestGetComponentStatusByPurl(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -162,7 +166,9 @@ func TestGetComponentStatusByPurl(t *testing.T) { } } -// TestGetProjectStatusByPurl tests retrieving project-level status only (no version info) +// TestGetProjectStatusByPurl tests retrieving project-level status only (no version info). +// +//goland:noinspection DuplicatedCode func TestGetProjectStatusByPurl(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { diff --git a/pkg/models/components.go b/pkg/models/components.go index 964a4d7..6830659 100644 --- a/pkg/models/components.go +++ b/pkg/models/components.go @@ -41,7 +41,7 @@ type Component struct { Component string `db:"component"` PurlType string `db:"purl_type"` PurlName string `db:"purl_name"` - Url string `db:"-"` + URL string `db:"-"` } func NewComponentModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext, likeOperator string) *ComponentModel { @@ -51,9 +51,8 @@ func NewComponentModel(ctx context.Context, s *zap.SugaredLogger, q *database.DB return &ComponentModel{ctx: ctx, s: s, q: q, likeOperator: likeOperator} } -// preProcessQueryJob Replace the clause #ORDER in the queries (if exist) according to the purlType +// preProcessQueryJob Replace the clause #ORDER in the queries (if exist) according to the purlType. func preProcessQueryJob(qListIn []QueryJob, purlType string) ([]QueryJob, error) { - if len(qListIn) == 0 { return []QueryJob{}, errors.New("cannot pre process query jobs empty or with limit less than 0") } @@ -308,7 +307,6 @@ func (m *ComponentModel) GetComponentsByVendorType(vendorName, purlType string, } func (m *ComponentModel) GetComponentsByNameVendorType(compName, vendor, purlType string, limit, offset int) ([]Component, error) { - if len(compName) == 0 || len(vendor) == 0 { m.s.Error("Please specify a valid Component Name to query") return []Component{}, errors.New("please specify a valid component Name to query") diff --git a/pkg/models/components_test.go b/pkg/models/components_test.go index b2a27a1..7bddbec 100644 --- a/pkg/models/components_test.go +++ b/pkg/models/components_test.go @@ -19,15 +19,17 @@ package models import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" myconfig "scanoss.com/components/pkg/config" - "testing" ) +//goland:noinspection DuplicatedCode func TestComponentsModel(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -103,7 +105,6 @@ func TestComponentsModel(t *testing.T) { t.Errorf("components.GetComponentsByVendorType() error = %v", err) } fmt.Printf("Components: %v\n", components) - } _, err = component.GetComponents("", "", 0, 0) @@ -128,7 +129,6 @@ func TestComponentsModel(t *testing.T) { } func TestPreProcessQueryJobs(t *testing.T) { - testTable := []struct { qList []QueryJob purlType string diff --git a/pkg/models/doc.go b/pkg/models/doc.go new file mode 100644 index 0000000..9e8bc26 --- /dev/null +++ b/pkg/models/doc.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Package models contains the models for the application +package models diff --git a/pkg/models/tests/bad_sql.sql b/pkg/models/tests/bad_sql.sql index 8bf8a8f..819bd4e 100644 --- a/pkg/models/tests/bad_sql.sql +++ b/pkg/models/tests/bad_sql.sql @@ -1,3 +1,3 @@ +-- noinspection AnnotatorForFile -- noinspection SyntaxErrorForFile - CREATE TABLE failed; \ No newline at end of file diff --git a/pkg/protocol/rest/server.go b/pkg/protocol/rest/server.go index 5faccbd..d331d13 100644 --- a/pkg/protocol/rest/server.go +++ b/pkg/protocol/rest/server.go @@ -42,8 +42,8 @@ func RunServer(config *myconfig.ServerConfig, ctx context.Context, grpcPort, htt go func() { ctx2, cancel := context.WithCancel(ctx) defer cancel() - if err := pb.RegisterComponentsHandlerFromEndpoint(ctx2, mux, grpcGateway, opts); err != nil { - zlog.S.Panicf("Failed to start HTTP gateway %v", err) + if err2 := pb.RegisterComponentsHandlerFromEndpoint(ctx2, mux, grpcGateway, opts); err2 != nil { + zlog.S.Panicf("Failed to start HTTP gateway %v", err2) } gw.StartGateway(srv, config.TLS.CertFile, config.TLS.KeyFile, startTLS) }() diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index 5dcb60b..cd634df 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -49,14 +49,14 @@ func NewComponentServer(db *sqlx.DB, config *myconfig.ServerConfig) pb.Component } } -// Echo sends back the same message received +// Echo sends back the same message received. func (d componentServer) Echo(ctx context.Context, request *common.EchoRequest) (*common.EchoResponse, error) { s := ctxzap.Extract(ctx).Sugar() s.Infof("Received (%v): %v", ctx, request.GetMessage()) return &common.EchoResponse{Message: request.GetMessage()}, nil } -// SearchComponents and retrieves a list of components +// SearchComponents and retrieves a list of components. func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompSearchRequest) (*pb.CompSearchResponse, error) { requestStartTime := time.Now() // Capture the scan start time s := ctxzap.Extract(ctx).Sugar() @@ -106,7 +106,6 @@ func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompS } func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.CompVersionRequest) (*pb.CompVersionResponse, error) { - requestStartTime := time.Now() // Capture the scan start time s := ctxzap.Extract(ctx).Sugar() s.Info("Processing component versions request...") @@ -171,7 +170,7 @@ func telemetryCompVersionRequestTime(ctx context.Context, config *myconfig.Serve } } -// GetComponentStatus retrieves status information for a specific component +// GetComponentStatus retrieves status information for a specific component. func (d componentServer) GetComponentStatus(ctx context.Context, request *common.ComponentRequest) (*pb.ComponentStatusResponse, error) { s := ctxzap.Extract(ctx).Sugar() s.Info("Processing component status request...") @@ -194,15 +193,11 @@ func (d componentServer) GetComponentStatus(ctx context.Context, request *common return &pb.ComponentStatusResponse{}, err } // Convert the output to protobuf - statusResponse, err := convertComponentStatusOutput(s, dtoOutput) - if err != nil { - s.Errorf("Failed to convert component status output: %v", err) - return &pb.ComponentStatusResponse{}, errors.New("problems encountered extracting component status data") - } + statusResponse := convertComponentStatusOutput(dtoOutput) return statusResponse, nil } -// GetComponentsStatus retrieves status information for multiple components +// GetComponentsStatus retrieves status information for multiple components. func (d componentServer) GetComponentsStatus(ctx context.Context, request *common.ComponentsRequest) (*pb.ComponentsStatusResponse, error) { s := ctxzap.Extract(ctx).Sugar() s.Info("Processing components status request...") @@ -231,16 +226,7 @@ func (d componentServer) GetComponentsStatus(ctx context.Context, request *commo return &pb.ComponentsStatusResponse{Status: status}, nil } // Convert the output to protobuf - statusResponse, err := convertComponentsStatusOutput(s, dtoOutput) - if err != nil { - s.Errorf("Failed to convert components status output: %v", err) - return &pb.ComponentsStatusResponse{Status: &common.StatusResponse{ - Status: common.StatusCode_FAILED, - Message: "Problems encountered extracting components status data", - Db: d.getDBVersion(), - Server: &common.StatusResponse_Server{Version: d.config.App.Version}, - }}, nil - } + statusResponse := convertComponentsStatusOutput(dtoOutput) // Set the status and respond with the data return &pb.ComponentsStatusResponse{ Components: statusResponse.Components, diff --git a/pkg/service/component_service_test.go b/pkg/service/component_service_test.go index 16eef0b..368c19a 100644 --- a/pkg/service/component_service_test.go +++ b/pkg/service/component_service_test.go @@ -32,6 +32,9 @@ import ( "scanoss.com/components/pkg/models" ) +const appVersion = "test-version" + +//goland:noinspection DuplicatedCode func TestComponentServer_Echo(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -49,7 +52,7 @@ func TestComponentServer_Echo(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) type args struct { @@ -87,6 +90,7 @@ func TestComponentServer_Echo(t *testing.T) { } } +//goland:noinspection DuplicatedCode func TestComponentServer_SearchComponents(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -108,7 +112,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) var compRequestData = `{ @@ -140,7 +144,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { ctx: ctx, req: &compReq, }, - want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No components found matching the search criteria", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No components found matching the search criteria", Server: &common.StatusResponse_Server{Version: appVersion}}}, }, { name: "Search for a empty request", @@ -149,7 +153,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { ctx: ctx, req: &pb.CompSearchRequest{}, }, - want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No data supplied", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No data supplied", Server: &common.StatusResponse_Server{Version: appVersion}}}, wantErr: false, }, } @@ -168,6 +172,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { } } +//goland:noinspection DuplicatedCode func TestComponentServer_GetComponentVersions(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -189,7 +194,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) var compVersionRequestData = `{ @@ -220,7 +225,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { ctx: ctx, req: &compVersionReq, }, - want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success", Server: &common.StatusResponse_Server{Version: appVersion}}}, }, { name: "Search for a empty request", @@ -229,7 +234,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { ctx: ctx, req: &pb.CompVersionRequest{}, }, - want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No purl supplied", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No purl supplied", Server: &common.StatusResponse_Server{Version: appVersion}}}, wantErr: false, }, } @@ -248,6 +253,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { } } +//goland:noinspection DuplicatedCode func TestComponentServer_GetComponentStatus(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -269,7 +275,7 @@ func TestComponentServer_GetComponentStatus(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) type args struct { diff --git a/pkg/service/component_support.go b/pkg/service/component_support.go index b5010a3..0e26a66 100644 --- a/pkg/service/component_support.go +++ b/pkg/service/component_support.go @@ -150,8 +150,7 @@ func convertComponentStatusInput(s *zap.SugaredLogger, request interface{}) (dto // // Returns: // - gRPC ComponentStatusResponse with populated fields, or error if conversion fails -func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentStatusOutput) (*pb.ComponentStatusResponse, error) { - +func convertComponentStatusOutput(output dtos.ComponentStatusOutput) *pb.ComponentStatusResponse { response := &pb.ComponentStatusResponse{ Purl: output.Purl, Requirement: output.Requirement, @@ -172,7 +171,7 @@ func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentSta ErrorMessage: output.ComponentStatus.ErrorMessage, ErrorCode: domain.StatusCodeToErrorCode(*output.ComponentStatus.ErrorCode), } - return response, nil + return response } if output.VersionStatus != nil { if output.VersionStatus.ErrorCode == nil { @@ -194,7 +193,7 @@ func convertComponentStatusOutput(s *zap.SugaredLogger, output dtos.ComponentSta } } - return response, nil + return response } // convertComponentsStatusInput converts a gRPC components status request into a ComponentsStatusInput DTO. @@ -229,17 +228,11 @@ func convertComponentsStatusInput(s *zap.SugaredLogger, request interface{}) (dt // // Returns: // - gRPC ComponentsStatusResponse with all converted component status entries, or error if conversion fails -func convertComponentsStatusOutput(s *zap.SugaredLogger, output dtos.ComponentsStatusOutput) (*pb.ComponentsStatusResponse, error) { - +func convertComponentsStatusOutput(output dtos.ComponentsStatusOutput) *pb.ComponentsStatusResponse { var statusResp pb.ComponentsStatusResponse - var someErr error = nil for _, c := range output.Components { - cs, errComp := convertComponentStatusOutput(s, c) - if errComp != nil { - someErr = errComp - } + cs := convertComponentStatusOutput(c) statusResp.Components = append(statusResp.Components, cs) } - - return &statusResp, someErr + return &statusResp } diff --git a/pkg/service/component_support_test.go b/pkg/service/component_support_test.go index 2efa0ba..28628b0 100644 --- a/pkg/service/component_support_test.go +++ b/pkg/service/component_support_test.go @@ -3,11 +3,12 @@ package service import ( "context" "fmt" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" pb "github.com/scanoss/papi/api/componentsv2" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" "scanoss.com/components/pkg/dtos" - "testing" ) func TestConvertSearchComponentInput(t *testing.T) { @@ -28,7 +29,6 @@ func TestConvertSearchComponentInput(t *testing.T) { t.Errorf("Error generating dto from protobuff request: %v\n", err) } fmt.Printf("dto component input: %v\n", dto) - } func TestConvertSearchComponentOutput(t *testing.T) { @@ -44,7 +44,7 @@ func TestConvertSearchComponentOutput(t *testing.T) { { Component: "angular", Purl: "pkg:github/bclinkinbeard/angular", - Url: "https://github.com/bclinkinbeard/angular", + URL: "https://github.com/bclinkinbeard/angular", }, }} @@ -87,14 +87,14 @@ func TestConvertCompVersionsOutput(t *testing.T) { Component: dtos.ComponentOutput{ Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", Versions: []dtos.ComponentVersion{ { Version: "1.8.3", Licenses: []dtos.ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -108,5 +108,4 @@ func TestConvertCompVersionsOutput(t *testing.T) { t.Errorf("Error converting dto to protobuff request: %v\n", err) } fmt.Printf("dto component input: %v\n", protobuffOut) - } diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index 07106d0..2d359af 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +// Package usecase contains the business logic for the components API. package usecase import ( @@ -21,17 +22,15 @@ import ( "errors" "fmt" - se "scanoss.com/components/pkg/errors" - "github.com/jmoiron/sqlx" cmpHelper "github.com/scanoss/go-component-helper/componenthelper" - "github.com/scanoss/go-grpc-helper/pkg/grpc/database" "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" purlhelper "github.com/scanoss/go-purl-helper/pkg" "go.uber.org/zap" "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/dtos" + se "scanoss.com/components/pkg/errors" "scanoss.com/components/pkg/models" ) @@ -40,7 +39,7 @@ type ComponentUseCase struct { s *zap.SugaredLogger q *database.DBQueryContext components *models.ComponentModel - allUrl *models.AllUrlsModel + allURL *models.AllURLsModel componentStatus *models.ComponentStatusModel db *sqlx.DB statusMapper *config.StatusMapper @@ -49,7 +48,7 @@ type ComponentUseCase struct { func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext, statusMapper *config.StatusMapper) *ComponentUseCase { return &ComponentUseCase{ctx: ctx, s: s, q: q, components: models.NewComponentModel(ctx, s, q, database.GetLikeOperator(db)), - allUrl: models.NewAllUrlModel(ctx, s, q), + allURL: models.NewAllURLModel(ctx, s, q), componentStatus: models.NewComponentStatusModel(ctx, s, q), db: db, statusMapper: statusMapper, @@ -59,20 +58,21 @@ func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *da func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (dtos.ComponentsSearchOutput, error) { var err error var searchResults []models.Component - if len(request.Search) != 0 { + switch { + case len(request.Search) != 0: searchResults, err = c.components.GetComponents(request.Search, request.Package, request.Limit, request.Offset) - } else if len(request.Component) != 0 && len(request.Vendor) == 0 { + case len(request.Component) != 0 && len(request.Vendor) == 0: searchResults, err = c.components.GetComponentsByNameType(request.Component, request.Package, request.Limit, request.Offset) - } else if len(request.Component) == 0 && len(request.Vendor) != 0 { + case len(request.Component) == 0 && len(request.Vendor) != 0: searchResults, err = c.components.GetComponentsByVendorType(request.Vendor, request.Package, request.Limit, request.Offset) - } else if len(request.Component) != 0 && len(request.Vendor) != 0 { + case len(request.Component) != 0 && len(request.Vendor) != 0: searchResults, err = c.components.GetComponentsByNameVendorType(request.Component, request.Vendor, request.Package, request.Limit, request.Offset) } if err != nil { c.s.Errorf("Problem encountered searching for components: %v - %v.", request.Component, request.Package) } for i := range searchResults { - searchResults[i].Url, _ = purlhelper.ProjectUrl(searchResults[i].PurlName, searchResults[i].PurlType) + searchResults[i].URL, _ = purlhelper.ProjectUrl(searchResults[i].PurlName, searchResults[i].PurlType) } var componentsSearchResults []dtos.ComponentSearchOutput @@ -81,7 +81,7 @@ func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (d componentSearchResult.Name = component.Component componentSearchResult.Component = component.Component // Deprecated. Remove in future versions componentSearchResult.Purl = "pkg:" + component.PurlType + "/" + component.PurlName - componentSearchResult.Url = component.Url + componentSearchResult.URL = component.URL componentsSearchResults = append(componentsSearchResults, componentSearchResult) } if len(componentsSearchResults) == 0 { @@ -95,7 +95,7 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp c.s.Errorf("The request does not contains purl to retrieve component versions") return dtos.ComponentVersionsOutput{}, errors.New("the request does not contains purl to retrieve component versions") } - allUrls, err := c.allUrl.GetUrlsByPurlString(request.Purl, request.Limit) + allUrls, err := c.allURL.GetUrlsByPurlString(request.Purl, request.Limit) if err != nil { c.s.Errorf("Problem encountered gettings URLs versions for: %v - %v.", request.Purl, err) return dtos.ComponentVersionsOutput{}, err @@ -116,7 +116,7 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp output.Purl = request.Purl if len(allUrls) > 0 { output.Name = allUrls[0].Component - output.Url = projectURL + output.URL = projectURL output.Component = allUrls[0].Component output.Versions = []dtos.ComponentVersion{} for _, u := range allUrls { @@ -135,7 +135,7 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp continue } license.Name = u.License - license.SpdxId = u.LicenseId + license.SpdxID = u.LicenseID license.IsSpdx = u.IsSpdx version.Licenses = append(version.Licenses, license) output.Versions = append(output.Versions, version) @@ -169,6 +169,7 @@ func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) // handleComponentStatusResult routes the component status result to the appropriate handler based on status code. func (c ComponentUseCase) handleComponentStatusResult(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + //nolint:exhaustive switch result.Status.StatusCode { case domain.Success: return c.handleSuccessStatus(request, result) diff --git a/pkg/usecase/component_test.go b/pkg/usecase/component_test.go index 259dc2c..ad89c80 100644 --- a/pkg/usecase/component_test.go +++ b/pkg/usecase/component_test.go @@ -31,6 +31,7 @@ import ( "scanoss.com/components/pkg/models" ) +//goland:noinspection DuplicatedCode func TestComponentUseCase_SearchComponents(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -85,11 +86,10 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { fmt.Printf("Component-only search failed as expected: %v\n", err) // This is fine - some component searches may not find exact matches } - } +//goland:noinspection DuplicatedCode func TestComponentUseCase_GetComponentVersions(t *testing.T) { - err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) @@ -149,10 +149,10 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { if err == nil { t.Errorf("an error was expected when getting components version %v\n", err) } - } } +//goland:noinspection DuplicatedCode func TestComponentUseCase_GetComponentStatus(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -220,6 +220,7 @@ func TestComponentUseCase_GetComponentStatus(t *testing.T) { } } +//goland:noinspection DuplicatedCode func TestComponentUseCase_GetComponentsStatus(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -286,7 +287,9 @@ func TestComponentUseCase_GetComponentsStatus(t *testing.T) { } } -// TestComponentUseCase_GetComponentStatus_AllCases tests all status code paths +// TestComponentUseCase_GetComponentStatus_AllCases tests all status code paths. +// +//goland:noinspection DuplicatedCode func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { From 46cd7f5588e26dc6d96292bb116c1250188e2da7 Mon Sep 17 00:00:00 2001 From: Sean Egan Date: Thu, 26 Mar 2026 18:09:52 +0000 Subject: [PATCH 13/13] update to latest dependency versions --- go.mod | 23 ++++++++++---------- go.sum | 66 ++++++++++++++++++++++++++++------------------------------ 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index e29ea7f..568a405 100644 --- a/go.mod +++ b/go.mod @@ -7,18 +7,18 @@ require ( github.com/google/go-cmp v0.7.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jmoiron/sqlx v1.4.0 - github.com/lib/pq v1.11.2 - github.com/scanoss/go-component-helper v0.4.0 + github.com/lib/pq v1.12.0 + github.com/scanoss/go-component-helper v0.5.0 github.com/scanoss/go-grpc-helper v0.13.0 github.com/scanoss/go-models v0.7.0 - github.com/scanoss/go-purl-helper v0.2.1 - github.com/scanoss/papi v0.32.1 + github.com/scanoss/go-purl-helper v0.3.0 + github.com/scanoss/papi v0.33.0 github.com/scanoss/zap-logging-helper v0.4.0 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 go.uber.org/zap v1.27.1 google.golang.org/grpc v1.79.3 - modernc.org/sqlite v1.46.1 + modernc.org/sqlite v1.47.0 ) // replace github.com/scanoss/papi => ../papi @@ -39,7 +39,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/package-url/packageurl-go v0.1.3 // indirect + github.com/package-url/packageurl-go v0.1.5 // indirect github.com/phuslu/iploc v1.0.20230201 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/scanoss/ipfilter/v2 v2.0.2 // indirect @@ -51,18 +51,17 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.50.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.67.6 // indirect + modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 5023412..6666116 100644 --- a/go.sum +++ b/go.sum @@ -593,8 +593,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -604,8 +604,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= -github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/package-url/packageurl-go v0.1.5 h1:O4efRXja2XQ5CtiiYiCZ22k/m7i5ugLiAghgcC+eDgk= +github.com/package-url/packageurl-go v0.1.5/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/phuslu/iploc v1.0.20230201 h1:AMhy7j8z0N5iI0jaqh514KTDEB7wVdQJ4Y4DJPCvKBU= github.com/phuslu/iploc v1.0.20230201/go.mod h1:gsgExGWldwv1AEzZm+Ki9/vGfyjkL33pbSr9HGpt2Xg= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -624,18 +624,18 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/scanoss/go-component-helper v0.4.0 h1:LrO7LPY8m9mEnaG4lKb6ssT410nFjEmm5EJPEaYRWHw= -github.com/scanoss/go-component-helper v0.4.0/go.mod h1:VWOGsu3JWI/NT53w7rOWL/yN/2jfwsEHdFqMvIH8TRI= +github.com/scanoss/go-component-helper v0.5.0 h1:9t+vZIGKQYGj7Bomv3uKWe6bwS5yRrAOP5zJOTrN1Sw= +github.com/scanoss/go-component-helper v0.5.0/go.mod h1:8RZU7jPsdwuQTB68yQ06yhrlHglU3vhn348EIiFuKaw= github.com/scanoss/go-grpc-helper v0.13.0 h1:GtKDKuc2jTtv27naIVF3f095tuJvSvxbeN+HmUdBS4Y= github.com/scanoss/go-grpc-helper v0.13.0/go.mod h1:mNfM/788jzo8rQ2K2Y97UYQPd6MjuVXEM7r05kWPOJc= github.com/scanoss/go-models v0.7.0 h1:6uhr3sGWIFz3wb1k+nhFfEoGbR8n5e5q3kJGJuLqdPc= github.com/scanoss/go-models v0.7.0/go.mod h1:gkXP2/3ZUn8IyH/z/528aTcWZ2mzlzsXiY4bzBbrM+o= -github.com/scanoss/go-purl-helper v0.2.1 h1:jp960a585ycyJSlqZky1NatMJBIQi/JGITDfNSu/9As= -github.com/scanoss/go-purl-helper v0.2.1/go.mod h1:v20/bKD8G+vGrILdiq6r0hyRD2bO8frCJlu9drEcQ38= +github.com/scanoss/go-purl-helper v0.3.0 h1:zH5rcYbmYTvKms2oWrYV+8rWZ2ElLgDIOy2jZ9XhAg0= +github.com/scanoss/go-purl-helper v0.3.0/go.mod h1:3CFUM/OuUp9Q58IF/yGkQhr+G4x6hJNmF8N1f0W82C4= github.com/scanoss/ipfilter/v2 v2.0.2 h1:GaB9i8kVJg9JQZm5XGStYkEpiaCVdsrj7ezI2wV/oh8= github.com/scanoss/ipfilter/v2 v2.0.2/go.mod h1:AwrpX4XGbZ7EKISMi1d6E5csBk1nWB8+ugpvXHFcTpA= -github.com/scanoss/papi v0.32.1 h1:TrnqkLBHjQ4X+wifg5KfIHeAzWfHVkcqaWtRkKxqKxk= -github.com/scanoss/papi v0.32.1/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= +github.com/scanoss/papi v0.33.0 h1:Qt7EABUapXzNlaFT2uIPIkpaHyCsLe2+PUPwn2iVkpA= +github.com/scanoss/papi v0.33.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= github.com/scanoss/zap-logging-helper v0.4.0 h1:2qTYoaFa9+MlD2/1wmPtiDHfh+42NIEwgKVU3rPpl0Y= github.com/scanoss/zap-logging-helper v0.4.0/go.mod h1:9QuEZcq73g/0Izv1tWeOWukoIK0oTBzM4jSNQ5kRR1w= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -678,22 +678,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= @@ -727,8 +727,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -757,8 +755,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -925,8 +923,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1007,8 +1005,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1273,18 +1271,18 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= -modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= -modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1293,8 +1291,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=