diff --git a/Cargo.lock b/Cargo.lock index 515368b..380ffd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,6 +601,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" @@ -908,6 +914,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy-regex" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -963,6 +992,8 @@ version = "0.1.1" dependencies = [ "async-nats", "futures", + "heck", + "lazy-regex", "rbx_asset", "rbx_binary", "rbx_dom_weak", diff --git a/openapi-internal.yaml b/openapi-internal.yaml index 1fb1fd7..656cc4d 100644 --- a/openapi-internal.yaml +++ b/openapi-internal.yaml @@ -73,6 +73,37 @@ paths: - Mapfixes parameters: - $ref: '#/components/parameters/MapfixID' + - name: ModelVersion + in: query + required: true + schema: + type: integer + format: int64 + minimum: 0 + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /mapfixes/{MapfixID}/status/validator-request-changes: + post: + summary: (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested + operationId: actionMapfixRequestChanges + tags: + - Mapfixes + parameters: + - $ref: '#/components/parameters/MapfixID' + - name: StatusMessage + in: query + required: true + schema: + type: string + minLength: 0 + maxLength: 4096 responses: "204": description: Successful response @@ -228,6 +259,37 @@ paths: - Submissions parameters: - $ref: '#/components/parameters/SubmissionID' + - name: ModelVersion + in: query + required: true + schema: + type: integer + format: int64 + minimum: 0 + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /submissions/{SubmissionID}/status/validator-request-changes: + post: + summary: (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested + operationId: actionSubmissionRequestChanges + tags: + - Submissions + parameters: + - $ref: '#/components/parameters/SubmissionID' + - name: StatusMessage + in: query + required: true + schema: + type: string + minLength: 0 + maxLength: 4096 responses: "204": description: Successful response diff --git a/pkg/internal/oas_client_gen.go b/pkg/internal/oas_client_gen.go index 3a33e68..78d4f40 100644 --- a/pkg/internal/oas_client_gen.go +++ b/pkg/internal/oas_client_gen.go @@ -34,6 +34,12 @@ type Invoker interface { // // POST /mapfixes/{MapfixID}/status/validator-failed ActionMapfixAccepted(ctx context.Context, params ActionMapfixAcceptedParams) error + // ActionMapfixRequestChanges invokes actionMapfixRequestChanges operation. + // + // (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. + // + // POST /mapfixes/{MapfixID}/status/validator-request-changes + ActionMapfixRequestChanges(ctx context.Context, params ActionMapfixRequestChangesParams) error // ActionMapfixSubmitted invokes actionMapfixSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -64,6 +70,12 @@ type Invoker interface { // // POST /submissions/{SubmissionID}/status/validator-failed ActionSubmissionAccepted(ctx context.Context, params ActionSubmissionAcceptedParams) error + // ActionSubmissionRequestChanges invokes actionSubmissionRequestChanges operation. + // + // (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. + // + // POST /submissions/{SubmissionID}/status/validator-request-changes + ActionSubmissionRequestChanges(ctx context.Context, params ActionSubmissionRequestChangesParams) error // ActionSubmissionSubmitted invokes actionSubmissionSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -294,6 +306,115 @@ func (c *Client) sendActionMapfixAccepted(ctx context.Context, params ActionMapf return result, nil } +// ActionMapfixRequestChanges invokes actionMapfixRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. +// +// POST /mapfixes/{MapfixID}/status/validator-request-changes +func (c *Client) ActionMapfixRequestChanges(ctx context.Context, params ActionMapfixRequestChangesParams) error { + _, err := c.sendActionMapfixRequestChanges(ctx, params) + return err +} + +func (c *Client) sendActionMapfixRequestChanges(ctx context.Context, params ActionMapfixRequestChangesParams) (res *ActionMapfixRequestChangesNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionMapfixRequestChanges"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/validator-request-changes"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ActionMapfixRequestChangesOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/mapfixes/" + { + // Encode "MapfixID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "MapfixID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.MapfixID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/status/validator-request-changes" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "StatusMessage" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "StatusMessage", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + return e.EncodeValue(conv.StringToString(params.StatusMessage)) + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeActionMapfixRequestChangesResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ActionMapfixSubmitted invokes actionMapfixSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -363,6 +484,24 @@ func (c *Client) sendActionMapfixSubmitted(ctx context.Context, params ActionMap pathParts[2] = "/status/validator-submitted" uri.AddPathParts(u, pathParts[:]...) + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "ModelVersion" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "ModelVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + return e.EncodeValue(conv.Int64ToString(params.ModelVersion)) + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + stage = "EncodeRequest" r, err := ht.NewRequest(ctx, "POST", u) if err != nil { @@ -785,6 +924,115 @@ func (c *Client) sendActionSubmissionAccepted(ctx context.Context, params Action return result, nil } +// ActionSubmissionRequestChanges invokes actionSubmissionRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. +// +// POST /submissions/{SubmissionID}/status/validator-request-changes +func (c *Client) ActionSubmissionRequestChanges(ctx context.Context, params ActionSubmissionRequestChangesParams) error { + _, err := c.sendActionSubmissionRequestChanges(ctx, params) + return err +} + +func (c *Client) sendActionSubmissionRequestChanges(ctx context.Context, params ActionSubmissionRequestChangesParams) (res *ActionSubmissionRequestChangesNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionSubmissionRequestChanges"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/validator-request-changes"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ActionSubmissionRequestChangesOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/submissions/" + { + // Encode "SubmissionID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "SubmissionID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.SubmissionID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/status/validator-request-changes" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "StatusMessage" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "StatusMessage", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + return e.EncodeValue(conv.StringToString(params.StatusMessage)) + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeActionSubmissionRequestChangesResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ActionSubmissionSubmitted invokes actionSubmissionSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -854,6 +1102,24 @@ func (c *Client) sendActionSubmissionSubmitted(ctx context.Context, params Actio pathParts[2] = "/status/validator-submitted" uri.AddPathParts(u, pathParts[:]...) + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "ModelVersion" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "ModelVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + return e.EncodeValue(conv.Int64ToString(params.ModelVersion)) + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + stage = "EncodeRequest" r, err := ht.NewRequest(ctx, "POST", u) if err != nil { diff --git a/pkg/internal/oas_handlers_gen.go b/pkg/internal/oas_handlers_gen.go index a205b0e..ff02638 100644 --- a/pkg/internal/oas_handlers_gen.go +++ b/pkg/internal/oas_handlers_gen.go @@ -183,6 +183,159 @@ func (s *Server) handleActionMapfixAcceptedRequest(args [1]string, argsEscaped b } } +// handleActionMapfixRequestChangesRequest handles actionMapfixRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. +// +// POST /mapfixes/{MapfixID}/status/validator-request-changes +func (s *Server) handleActionMapfixRequestChangesRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionMapfixRequestChanges"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/validator-request-changes"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixRequestChangesOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ActionMapfixRequestChangesOperation, + ID: "actionMapfixRequestChanges", + } + ) + params, err := decodeActionMapfixRequestChangesParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *ActionMapfixRequestChangesNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ActionMapfixRequestChangesOperation, + OperationSummary: "(Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested", + OperationID: "actionMapfixRequestChanges", + Body: nil, + Params: middleware.Parameters{ + { + Name: "MapfixID", + In: "path", + }: params.MapfixID, + { + Name: "StatusMessage", + In: "query", + }: params.StatusMessage, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = ActionMapfixRequestChangesParams + Response = *ActionMapfixRequestChangesNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackActionMapfixRequestChangesParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.ActionMapfixRequestChanges(ctx, params) + return response, err + }, + ) + } else { + err = s.h.ActionMapfixRequestChanges(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeActionMapfixRequestChangesResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleActionMapfixSubmittedRequest handles actionMapfixSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -281,6 +434,10 @@ func (s *Server) handleActionMapfixSubmittedRequest(args [1]string, argsEscaped Name: "MapfixID", In: "path", }: params.MapfixID, + { + Name: "ModelVersion", + In: "query", + }: params.ModelVersion, }, Raw: r, } @@ -936,6 +1093,159 @@ func (s *Server) handleActionSubmissionAcceptedRequest(args [1]string, argsEscap } } +// handleActionSubmissionRequestChangesRequest handles actionSubmissionRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. +// +// POST /submissions/{SubmissionID}/status/validator-request-changes +func (s *Server) handleActionSubmissionRequestChangesRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionSubmissionRequestChanges"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/validator-request-changes"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionRequestChangesOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ActionSubmissionRequestChangesOperation, + ID: "actionSubmissionRequestChanges", + } + ) + params, err := decodeActionSubmissionRequestChangesParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *ActionSubmissionRequestChangesNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ActionSubmissionRequestChangesOperation, + OperationSummary: "(Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested", + OperationID: "actionSubmissionRequestChanges", + Body: nil, + Params: middleware.Parameters{ + { + Name: "SubmissionID", + In: "path", + }: params.SubmissionID, + { + Name: "StatusMessage", + In: "query", + }: params.StatusMessage, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = ActionSubmissionRequestChangesParams + Response = *ActionSubmissionRequestChangesNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackActionSubmissionRequestChangesParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.ActionSubmissionRequestChanges(ctx, params) + return response, err + }, + ) + } else { + err = s.h.ActionSubmissionRequestChanges(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeActionSubmissionRequestChangesResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleActionSubmissionSubmittedRequest handles actionSubmissionSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -1034,6 +1344,10 @@ func (s *Server) handleActionSubmissionSubmittedRequest(args [1]string, argsEsca Name: "SubmissionID", In: "path", }: params.SubmissionID, + { + Name: "ModelVersion", + In: "query", + }: params.ModelVersion, }, Raw: r, } diff --git a/pkg/internal/oas_operations_gen.go b/pkg/internal/oas_operations_gen.go index cc5e6af..9ddc04b 100644 --- a/pkg/internal/oas_operations_gen.go +++ b/pkg/internal/oas_operations_gen.go @@ -7,11 +7,13 @@ type OperationName = string const ( ActionMapfixAcceptedOperation OperationName = "ActionMapfixAccepted" + ActionMapfixRequestChangesOperation OperationName = "ActionMapfixRequestChanges" ActionMapfixSubmittedOperation OperationName = "ActionMapfixSubmitted" ActionMapfixUploadedOperation OperationName = "ActionMapfixUploaded" ActionMapfixValidatedOperation OperationName = "ActionMapfixValidated" ActionOperationFailedOperation OperationName = "ActionOperationFailed" ActionSubmissionAcceptedOperation OperationName = "ActionSubmissionAccepted" + ActionSubmissionRequestChangesOperation OperationName = "ActionSubmissionRequestChanges" ActionSubmissionSubmittedOperation OperationName = "ActionSubmissionSubmitted" ActionSubmissionUploadedOperation OperationName = "ActionSubmissionUploaded" ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated" diff --git a/pkg/internal/oas_parameters_gen.go b/pkg/internal/oas_parameters_gen.go index f564af1..d21e5e5 100644 --- a/pkg/internal/oas_parameters_gen.go +++ b/pkg/internal/oas_parameters_gen.go @@ -159,13 +159,14 @@ func decodeActionMapfixAcceptedParams(args [1]string, argsEscaped bool, r *http. return params, nil } -// ActionMapfixSubmittedParams is parameters of actionMapfixSubmitted operation. -type ActionMapfixSubmittedParams struct { +// ActionMapfixRequestChangesParams is parameters of actionMapfixRequestChanges operation. +type ActionMapfixRequestChangesParams struct { // The unique identifier for a submission. - MapfixID int64 + MapfixID int64 + StatusMessage string } -func unpackActionMapfixSubmittedParams(packed middleware.Parameters) (params ActionMapfixSubmittedParams) { +func unpackActionMapfixRequestChangesParams(packed middleware.Parameters) (params ActionMapfixRequestChangesParams) { { key := middleware.ParameterKey{ Name: "MapfixID", @@ -173,10 +174,18 @@ func unpackActionMapfixSubmittedParams(packed middleware.Parameters) (params Act } params.MapfixID = packed[key].(int64) } + { + key := middleware.ParameterKey{ + Name: "StatusMessage", + In: "query", + } + params.StatusMessage = packed[key].(string) + } return params } -func decodeActionMapfixSubmittedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionMapfixSubmittedParams, _ error) { +func decodeActionMapfixRequestChangesParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionMapfixRequestChangesParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) // Decode path: MapfixID. if err := func() error { param := args[0] @@ -239,6 +248,203 @@ func decodeActionMapfixSubmittedParams(args [1]string, argsEscaped bool, r *http Err: err, } } + // Decode query: StatusMessage. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "StatusMessage", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.StatusMessage = c + return nil + }); err != nil { + return err + } + if err := func() error { + if err := (validate.String{ + MinLength: 0, + MinLengthSet: true, + MaxLength: 4096, + MaxLengthSet: true, + Email: false, + Hostname: false, + Regex: nil, + }).Validate(string(params.StatusMessage)); err != nil { + return errors.Wrap(err, "string") + } + return nil + }(); err != nil { + return err + } + } else { + return err + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "StatusMessage", + In: "query", + Err: err, + } + } + return params, nil +} + +// ActionMapfixSubmittedParams is parameters of actionMapfixSubmitted operation. +type ActionMapfixSubmittedParams struct { + // The unique identifier for a submission. + MapfixID int64 + ModelVersion int64 +} + +func unpackActionMapfixSubmittedParams(packed middleware.Parameters) (params ActionMapfixSubmittedParams) { + { + key := middleware.ParameterKey{ + Name: "MapfixID", + In: "path", + } + params.MapfixID = packed[key].(int64) + } + { + key := middleware.ParameterKey{ + Name: "ModelVersion", + In: "query", + } + params.ModelVersion = packed[key].(int64) + } + return params +} + +func decodeActionMapfixSubmittedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionMapfixSubmittedParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode path: MapfixID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "MapfixID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.MapfixID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.MapfixID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "MapfixID", + In: "path", + Err: err, + } + } + // Decode query: ModelVersion. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "ModelVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.ModelVersion = c + return nil + }); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.ModelVersion)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return err + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "ModelVersion", + In: "query", + Err: err, + } + } return params, nil } @@ -696,13 +902,14 @@ func decodeActionSubmissionAcceptedParams(args [1]string, argsEscaped bool, r *h return params, nil } -// ActionSubmissionSubmittedParams is parameters of actionSubmissionSubmitted operation. -type ActionSubmissionSubmittedParams struct { +// ActionSubmissionRequestChangesParams is parameters of actionSubmissionRequestChanges operation. +type ActionSubmissionRequestChangesParams struct { // The unique identifier for a submission. - SubmissionID int64 + SubmissionID int64 + StatusMessage string } -func unpackActionSubmissionSubmittedParams(packed middleware.Parameters) (params ActionSubmissionSubmittedParams) { +func unpackActionSubmissionRequestChangesParams(packed middleware.Parameters) (params ActionSubmissionRequestChangesParams) { { key := middleware.ParameterKey{ Name: "SubmissionID", @@ -710,10 +917,18 @@ func unpackActionSubmissionSubmittedParams(packed middleware.Parameters) (params } params.SubmissionID = packed[key].(int64) } + { + key := middleware.ParameterKey{ + Name: "StatusMessage", + In: "query", + } + params.StatusMessage = packed[key].(string) + } return params } -func decodeActionSubmissionSubmittedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionSubmissionSubmittedParams, _ error) { +func decodeActionSubmissionRequestChangesParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionSubmissionRequestChangesParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) // Decode path: SubmissionID. if err := func() error { param := args[0] @@ -776,6 +991,203 @@ func decodeActionSubmissionSubmittedParams(args [1]string, argsEscaped bool, r * Err: err, } } + // Decode query: StatusMessage. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "StatusMessage", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.StatusMessage = c + return nil + }); err != nil { + return err + } + if err := func() error { + if err := (validate.String{ + MinLength: 0, + MinLengthSet: true, + MaxLength: 4096, + MaxLengthSet: true, + Email: false, + Hostname: false, + Regex: nil, + }).Validate(string(params.StatusMessage)); err != nil { + return errors.Wrap(err, "string") + } + return nil + }(); err != nil { + return err + } + } else { + return err + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "StatusMessage", + In: "query", + Err: err, + } + } + return params, nil +} + +// ActionSubmissionSubmittedParams is parameters of actionSubmissionSubmitted operation. +type ActionSubmissionSubmittedParams struct { + // The unique identifier for a submission. + SubmissionID int64 + ModelVersion int64 +} + +func unpackActionSubmissionSubmittedParams(packed middleware.Parameters) (params ActionSubmissionSubmittedParams) { + { + key := middleware.ParameterKey{ + Name: "SubmissionID", + In: "path", + } + params.SubmissionID = packed[key].(int64) + } + { + key := middleware.ParameterKey{ + Name: "ModelVersion", + In: "query", + } + params.ModelVersion = packed[key].(int64) + } + return params +} + +func decodeActionSubmissionSubmittedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionSubmissionSubmittedParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode path: SubmissionID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "SubmissionID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.SubmissionID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.SubmissionID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "SubmissionID", + In: "path", + Err: err, + } + } + // Decode query: ModelVersion. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "ModelVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.ModelVersion = c + return nil + }); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.ModelVersion)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return err + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "ModelVersion", + In: "query", + Err: err, + } + } return params, nil } diff --git a/pkg/internal/oas_response_decoders_gen.go b/pkg/internal/oas_response_decoders_gen.go index 8b7f1da..77616ac 100644 --- a/pkg/internal/oas_response_decoders_gen.go +++ b/pkg/internal/oas_response_decoders_gen.go @@ -75,6 +75,66 @@ func decodeActionMapfixAcceptedResponse(resp *http.Response) (res *ActionMapfixA return res, errors.Wrap(defRes, "error") } +func decodeActionMapfixRequestChangesResponse(resp *http.Response) (res *ActionMapfixRequestChangesNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &ActionMapfixRequestChangesNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeActionMapfixSubmittedResponse(resp *http.Response) (res *ActionMapfixSubmittedNoContent, _ error) { switch resp.StatusCode { case 204: @@ -375,6 +435,66 @@ func decodeActionSubmissionAcceptedResponse(resp *http.Response) (res *ActionSub return res, errors.Wrap(defRes, "error") } +func decodeActionSubmissionRequestChangesResponse(resp *http.Response) (res *ActionSubmissionRequestChangesNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &ActionSubmissionRequestChangesNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeActionSubmissionSubmittedResponse(resp *http.Response) (res *ActionSubmissionSubmittedNoContent, _ error) { switch resp.StatusCode { case 204: diff --git a/pkg/internal/oas_response_encoders_gen.go b/pkg/internal/oas_response_encoders_gen.go index 38bfb96..024126c 100644 --- a/pkg/internal/oas_response_encoders_gen.go +++ b/pkg/internal/oas_response_encoders_gen.go @@ -20,6 +20,13 @@ func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, return nil } +func encodeActionMapfixRequestChangesResponse(response *ActionMapfixRequestChangesNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeActionMapfixSubmittedResponse(response *ActionMapfixSubmittedNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) @@ -55,6 +62,13 @@ func encodeActionSubmissionAcceptedResponse(response *ActionSubmissionAcceptedNo return nil } +func encodeActionSubmissionRequestChangesResponse(response *ActionSubmissionRequestChangesNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeActionSubmissionSubmittedResponse(response *ActionSubmissionSubmittedNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) diff --git a/pkg/internal/oas_router_gen.go b/pkg/internal/oas_router_gen.go index 3e9fba8..43be2cc 100644 --- a/pkg/internal/oas_router_gen.go +++ b/pkg/internal/oas_router_gen.go @@ -147,6 +147,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + case 'r': // Prefix: "request-changes" + + if l := len("request-changes"); len(elem) >= l && elem[0:l] == "request-changes" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleActionMapfixRequestChangesRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + case 's': // Prefix: "submitted" if l := len("submitted"); len(elem) >= l && elem[0:l] == "submitted" { @@ -476,6 +498,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + case 'r': // Prefix: "request-changes" + + if l := len("request-changes"); len(elem) >= l && elem[0:l] == "request-changes" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleActionSubmissionRequestChangesRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + case 's': // Prefix: "submitted" if l := len("submitted"); len(elem) >= l && elem[0:l] == "submitted" { @@ -760,6 +804,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } } + case 'r': // Prefix: "request-changes" + + if l := len("request-changes"); len(elem) >= l && elem[0:l] == "request-changes" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ActionMapfixRequestChangesOperation + r.summary = "(Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested" + r.operationID = "actionMapfixRequestChanges" + r.pathPattern = "/mapfixes/{MapfixID}/status/validator-request-changes" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + case 's': // Prefix: "submitted" if l := len("submitted"); len(elem) >= l && elem[0:l] == "submitted" { @@ -1127,6 +1195,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } } + case 'r': // Prefix: "request-changes" + + if l := len("request-changes"); len(elem) >= l && elem[0:l] == "request-changes" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ActionSubmissionRequestChangesOperation + r.summary = "(Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested" + r.operationID = "actionSubmissionRequestChanges" + r.pathPattern = "/submissions/{SubmissionID}/status/validator-request-changes" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + case 's': // Prefix: "submitted" if l := len("submitted"); len(elem) >= l && elem[0:l] == "submitted" { diff --git a/pkg/internal/oas_schemas_gen.go b/pkg/internal/oas_schemas_gen.go index e68e85a..f8c3cd4 100644 --- a/pkg/internal/oas_schemas_gen.go +++ b/pkg/internal/oas_schemas_gen.go @@ -13,6 +13,9 @@ func (s *ErrorStatusCode) Error() string { // ActionMapfixAcceptedNoContent is response for ActionMapfixAccepted operation. type ActionMapfixAcceptedNoContent struct{} +// ActionMapfixRequestChangesNoContent is response for ActionMapfixRequestChanges operation. +type ActionMapfixRequestChangesNoContent struct{} + // ActionMapfixSubmittedNoContent is response for ActionMapfixSubmitted operation. type ActionMapfixSubmittedNoContent struct{} @@ -28,6 +31,9 @@ type ActionOperationFailedNoContent struct{} // ActionSubmissionAcceptedNoContent is response for ActionSubmissionAccepted operation. type ActionSubmissionAcceptedNoContent struct{} +// ActionSubmissionRequestChangesNoContent is response for ActionSubmissionRequestChanges operation. +type ActionSubmissionRequestChangesNoContent struct{} + // ActionSubmissionSubmittedNoContent is response for ActionSubmissionSubmitted operation. type ActionSubmissionSubmittedNoContent struct{} diff --git a/pkg/internal/oas_server_gen.go b/pkg/internal/oas_server_gen.go index 94c8192..7350a9c 100644 --- a/pkg/internal/oas_server_gen.go +++ b/pkg/internal/oas_server_gen.go @@ -14,6 +14,12 @@ type Handler interface { // // POST /mapfixes/{MapfixID}/status/validator-failed ActionMapfixAccepted(ctx context.Context, params ActionMapfixAcceptedParams) error + // ActionMapfixRequestChanges implements actionMapfixRequestChanges operation. + // + // (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. + // + // POST /mapfixes/{MapfixID}/status/validator-request-changes + ActionMapfixRequestChanges(ctx context.Context, params ActionMapfixRequestChangesParams) error // ActionMapfixSubmitted implements actionMapfixSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -44,6 +50,12 @@ type Handler interface { // // POST /submissions/{SubmissionID}/status/validator-failed ActionSubmissionAccepted(ctx context.Context, params ActionSubmissionAcceptedParams) error + // ActionSubmissionRequestChanges implements actionSubmissionRequestChanges operation. + // + // (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. + // + // POST /submissions/{SubmissionID}/status/validator-request-changes + ActionSubmissionRequestChanges(ctx context.Context, params ActionSubmissionRequestChangesParams) error // ActionSubmissionSubmitted implements actionSubmissionSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. diff --git a/pkg/internal/oas_unimplemented_gen.go b/pkg/internal/oas_unimplemented_gen.go index 7baa6e9..46a4c7e 100644 --- a/pkg/internal/oas_unimplemented_gen.go +++ b/pkg/internal/oas_unimplemented_gen.go @@ -22,6 +22,15 @@ func (UnimplementedHandler) ActionMapfixAccepted(ctx context.Context, params Act return ht.ErrNotImplemented } +// ActionMapfixRequestChanges implements actionMapfixRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. +// +// POST /mapfixes/{MapfixID}/status/validator-request-changes +func (UnimplementedHandler) ActionMapfixRequestChanges(ctx context.Context, params ActionMapfixRequestChangesParams) error { + return ht.ErrNotImplemented +} + // ActionMapfixSubmitted implements actionMapfixSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. @@ -67,6 +76,15 @@ func (UnimplementedHandler) ActionSubmissionAccepted(ctx context.Context, params return ht.ErrNotImplemented } +// ActionSubmissionRequestChanges implements actionSubmissionRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> ChangesRequested. +// +// POST /submissions/{SubmissionID}/status/validator-request-changes +func (UnimplementedHandler) ActionSubmissionRequestChanges(ctx context.Context, params ActionSubmissionRequestChangesParams) error { + return ht.ErrNotImplemented +} + // ActionSubmissionSubmitted implements actionSubmissionSubmitted operation. // // (Internal endpoint) Role Validator changes status from Submitting -> Submitted. diff --git a/pkg/model/nats.go b/pkg/model/nats.go index 7a69450..0808a89 100644 --- a/pkg/model/nats.go +++ b/pkg/model/nats.go @@ -17,6 +17,17 @@ type CreateMapfixRequest struct { TargetAssetID uint64 } + +type CheckSubmissionRequest struct{ + SubmissionID int64 + ModelID uint64 +} + +type CheckMapfixRequest struct{ + MapfixID int64 + ModelID uint64 +} + type ValidateSubmissionRequest struct { // submission_id is passed back in the response message SubmissionID int64 diff --git a/pkg/service/mapfixes.go b/pkg/service/mapfixes.go index 18207f2..20529fd 100644 --- a/pkg/service/mapfixes.go +++ b/pkg/service/mapfixes.go @@ -490,7 +490,7 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac } // transaction - target_status := model.MapfixStatusSubmitted + target_status := model.MapfixStatusSubmitting smap := datastore.Optional() smap.Add("status_id", target_status) err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUnderConstruction, model.MapfixStatusChangesRequested}, smap) @@ -498,6 +498,21 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac return err } + validate_request := model.CheckMapfixRequest{ + MapfixID: mapfix.ID, + ModelID: mapfix.AssetID, + } + + j, err := json.Marshal(validate_request) + if err != nil { + return err + } + + _, err = svc.Nats.Publish("maptest.mapfixes.check", []byte(j)) + if err != nil { + return err + } + event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } diff --git a/pkg/service/submissions.go b/pkg/service/submissions.go index c81c24a..3487fac 100644 --- a/pkg/service/submissions.go +++ b/pkg/service/submissions.go @@ -511,7 +511,7 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap } // transaction - target_status := model.SubmissionStatusSubmitted + target_status := model.SubmissionStatusSubmitting smap := datastore.Optional() smap.Add("status_id", target_status) err = svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.SubmissionStatus{model.SubmissionStatusUnderConstruction, model.SubmissionStatusChangesRequested}, smap) @@ -519,6 +519,21 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap return err } + validate_request := model.CheckSubmissionRequest{ + SubmissionID: submission.ID, + ModelID: submission.AssetID, + } + + j, err := json.Marshal(validate_request) + if err != nil { + return err + } + + _, err = svc.Nats.Publish("maptest.submissions.check", []byte(j)) + if err != nil { + return err + } + event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } diff --git a/pkg/service_internal/mapfixes.go b/pkg/service_internal/mapfixes.go index 0827424..8d8b576 100644 --- a/pkg/service_internal/mapfixes.go +++ b/pkg/service_internal/mapfixes.go @@ -74,7 +74,7 @@ func (svc *Service) UpdateMapfixValidatedModel(ctx context.Context, params inter return nil } -// ActionMapfixValidate invokes actionMapfixValidate operation. +// ActionMapfixSubmitted invokes actionMapfixSubmitted operation. // // Role Validator changes status from Submitting -> Submitted. // @@ -84,6 +84,47 @@ func (svc *Service) ActionMapfixSubmitted(ctx context.Context, params internal.A target_status := model.MapfixStatusSubmitted smap := datastore.Optional() smap.Add("status_id", target_status) + smap.Add("asset_version", params.ModelVersion) + err := svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitting}, smap) + if err != nil { + return err + } + + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + EventData, err := json.Marshal(event_data) + if err != nil { + return err + } + + _, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{ + ID: 0, + User: ValidtorUserID, + ResourceType: model.ResourceMapfix, + ResourceID: params.MapfixID, + EventType: model.AuditEventTypeAction, + EventData: EventData, + }) + if err != nil { + return err + } + + return nil +} + +// ActionMapfixRequestChanges implements actionMapfixRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> RequestChanges. +// +// POST /mapfixes/{MapfixID}/status/validator-request-changes +func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params internal.ActionMapfixRequestChangesParams) error { + // transaction + target_status := model.MapfixStatusChangesRequested + smap := datastore.Optional() + smap.Add("status_id", target_status) + smap.Add("status_message", params.StatusMessage) err := svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitting}, smap) if err != nil { return err diff --git a/pkg/service_internal/submissions.go b/pkg/service_internal/submissions.go index 3250ca7..5b46efd 100644 --- a/pkg/service_internal/submissions.go +++ b/pkg/service_internal/submissions.go @@ -73,7 +73,7 @@ func (svc *Service) UpdateSubmissionValidatedModel(ctx context.Context, params i return nil } -// ActionSubmissionValidate invokes actionSubmissionValidate operation. +// ActionSubmissionSubmitted invokes actionSubmissionSubmitted operation. // // Role Validator changes status from Submitting -> Submitted. // @@ -83,6 +83,47 @@ func (svc *Service) ActionSubmissionSubmitted(ctx context.Context, params intern target_status := model.SubmissionStatusSubmitted smap := datastore.Optional() smap.Add("status_id", target_status) + smap.Add("asset_version", params.ModelVersion) + err := svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.SubmissionStatus{model.SubmissionStatusSubmitting}, smap) + if err != nil { + return err + } + + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + EventData, err := json.Marshal(event_data) + if err != nil { + return err + } + + _, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{ + ID: 0, + User: ValidtorUserID, + ResourceType: model.ResourceSubmission, + ResourceID: params.SubmissionID, + EventType: model.AuditEventTypeAction, + EventData: EventData, + }) + if err != nil { + return err + } + + return nil +} + +// ActionSubmissionRequestChanges implements actionSubmissionRequestChanges operation. +// +// (Internal endpoint) Role Validator changes status from Submitting -> RequestChanges. +// +// POST /submissions/{SubmissionID}/status/validator-request-changes +func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params internal.ActionSubmissionRequestChangesParams) error { + // transaction + target_status := model.SubmissionStatusChangesRequested + smap := datastore.Optional() + smap.Add("status_id", target_status) + smap.Add("status_message", params.StatusMessage) err := svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.SubmissionStatus{model.SubmissionStatusSubmitting}, smap) if err != nil { return err diff --git a/validation/Cargo.toml b/validation/Cargo.toml index e30338d..5f65441 100644 --- a/validation/Cargo.toml +++ b/validation/Cargo.toml @@ -16,3 +16,5 @@ serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" siphasher = "1.0.1" tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] } +heck = "0.5.0" +lazy-regex = "3.4.1" diff --git a/validation/api/src/internal.rs b/validation/api/src/internal.rs index 19d3c4e..a281dcc 100644 --- a/validation/api/src/internal.rs +++ b/validation/api/src/internal.rs @@ -162,6 +162,12 @@ impl Context{ .json().await.map_err(Error::ReqwestJson) } // simple submission endpoints + action!("submissions",action_submission_request_changes,config,ActionSubmissionRequestChangesRequest,"status/validator-request-changes",config.SubmissionID, + ("StatusMessage",config.StatusMessage.as_str()) + ); + action!("submissions",action_submission_submitted,config,ActionSubmissionSubmittedRequest,"status/validator-submitted",config.SubmissionID, + ("ModelVersion",config.ModelVersion.to_string().as_str()) + ); action!("submissions",action_submission_validated,config,SubmissionID,"status/validator-validated",config.0,); action!("submissions",update_submission_validated_model,config,UpdateSubmissionModelRequest,"validated-model",config.SubmissionID, ("ValidatedModelID",config.ModelID.to_string().as_str()) @@ -185,6 +191,12 @@ impl Context{ .json().await.map_err(Error::ReqwestJson) } // simple mapfixes endpoints + action!("mapfixes",action_mapfix_request_changes,config,ActionMapfixRequestChangesRequest,"status/validator-request-changes",config.MapfixID, + ("StatusMessage",config.StatusMessage.as_str()) + ); + action!("mapfixes",action_mapfix_submitted,config,ActionMapfixSubmittedRequest,"status/validator-submitted",config.MapfixID, + ("ModelVersion",config.ModelVersion.to_string().as_str()) + ); action!("mapfixes",action_mapfix_validated,config,MapfixID,"status/validator-validated",config.0,); action!("mapfixes",update_mapfix_validated_model,config,UpdateMapfixModelRequest,"validated-model",config.MapfixID, ("ValidatedModelID",config.ModelID.to_string().as_str()) diff --git a/validation/api/src/types.rs b/validation/api/src/types.rs index 580a97e..9ff38bf 100644 --- a/validation/api/src/types.rs +++ b/validation/api/src/types.rs @@ -222,6 +222,20 @@ pub struct UpdateSubmissionModelRequest{ pub ModelVersion:u64, } +#[allow(nonstandard_style)] +#[derive(Clone,Debug)] +pub struct ActionSubmissionSubmittedRequest{ + pub SubmissionID:i64, + pub ModelVersion:u64, +} + +#[allow(nonstandard_style)] +#[derive(Clone,Debug)] +pub struct ActionSubmissionRequestChangesRequest{ + pub SubmissionID:i64, + pub StatusMessage:String, +} + #[allow(nonstandard_style)] #[derive(Clone,Debug)] pub struct ActionSubmissionUploadedRequest{ @@ -247,6 +261,20 @@ pub struct UpdateMapfixModelRequest{ pub ModelVersion:u64, } +#[allow(nonstandard_style)] +#[derive(Clone,Debug)] +pub struct ActionMapfixSubmittedRequest{ + pub MapfixID:i64, + pub ModelVersion:u64, +} + +#[allow(nonstandard_style)] +#[derive(Clone,Debug)] +pub struct ActionMapfixRequestChangesRequest{ + pub MapfixID:i64, + pub StatusMessage:String, +} + #[allow(nonstandard_style)] #[derive(Clone,Debug)] pub struct ActionMapfixUploadedRequest{ diff --git a/validation/src/check.rs b/validation/src/check.rs new file mode 100644 index 0000000..87cdcc6 --- /dev/null +++ b/validation/src/check.rs @@ -0,0 +1,315 @@ +use crate::download::download_asset_version; +use crate::rbx_util::{class_is_a,get_mapinfo,get_root_instance,read_dom,MapInfo,ReadDomError}; + +use heck::{ToSnakeCase,ToTitleCase}; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + ModelInfoDownload(rbx_asset::cloud::GetError), + CreatorTypeMustBeUser, + ParseUserID(core::num::ParseIntError), + ParseModelVersion(core::num::ParseIntError), + Download(crate::download::Error), + ModelFileDecode(ReadDomError), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +#[allow(nonstandard_style)] +pub struct CheckRequest{ + pub ModelID:u64, +} + +impl From for CheckRequest{ + fn from(value:crate::nats_types::CheckMapfixRequest)->Self{ + Self{ + ModelID:value.ModelID, + } + } +} +impl From for CheckRequest{ + fn from(value:crate::nats_types::CheckSubmissionRequest)->Self{ + Self{ + ModelID:value.ModelID, + } + } +} + +#[derive(Default)] +pub struct CheckReport{ + // === METADATA CHECKS === + // the model must have exactly 1 root part (models uploaded to roblox can have multiple roots) + exactly_one_root:bool, + // the root must be of class Model + root_is_model:bool, + // the prefix of the model's name must match the game it was submitted for. bhop_ for bhop, and surf_ for surf + model_name_prefix_is_valid:bool, + // your model's name must match this regex: ^[a-z0-9_] + model_name_is_snake_case:bool, + // map must have a StringValue named Creator and DisplayName. additionally, they must both have a value + has_display_name:bool, + has_creator:bool, + // the display name must be capitalized + display_name_is_title_case:bool, + // you cannot have any Scripts or ModuleScripts that have the keyword 'getfenv" or 'require' + // you cannot have more than 50 duplicate scripts + + // === MODE CHECKS === + // Exactly one MapStart + exactly_one_mapstart:bool, + // At least one MapFinish + at_least_one_mapfinish:bool, + // Spawn0 or Spawn1 must exist + spawn1_exists:bool, + // No duplicate Spawn# + no_duplicate_spawns:bool, + // No duplicate WormholeOut# (duplicate WormholeIn# ok) + no_duplicate_wormhole_out:bool, +} +impl CheckReport{ + pub fn pass(&self)->bool{ + return self.exactly_one_root + &&self.root_is_model + &&self.model_name_prefix_is_valid + &&self.model_name_is_snake_case + &&self.has_display_name + &&self.has_creator + &&self.display_name_is_title_case + &&self.exactly_one_mapstart + &&self.at_least_one_mapfinish + &&self.spawn1_exists + &&self.no_duplicate_spawns + &&self.no_duplicate_wormhole_out + } +} +impl std::fmt::Display for CheckReport{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f, + "exactly_one_root={}\ +root_is_model={}\ +model_name_prefix_is_valid={}\ +model_name_is_snake_case={}\ +has_display_name={}\ +has_creator={}\ +display_name_is_title_case={}\ +exactly_one_mapstart={}\ +at_least_one_mapfinish={}\ +spawn1_exists={}\ +no_duplicate_spawns={}\ +no_duplicate_wormhole_out={}", + self.exactly_one_root, + self.root_is_model, + self.model_name_prefix_is_valid, + self.model_name_is_snake_case, + self.has_display_name, + self.has_creator, + self.display_name_is_title_case, + self.exactly_one_mapstart, + self.at_least_one_mapfinish, + self.spawn1_exists, + self.no_duplicate_spawns, + self.no_duplicate_wormhole_out, + ) + } +} + +enum Zone{ + Start(ModeID), + Finish(ModeID), +} + +#[derive(Debug,Hash,Eq,PartialEq)] +struct ModeID(u64); +impl ModeID{ + const MAIN:Self=Self(0); + const BONUS:Self=Self(1); +} +#[allow(dead_code)] +pub enum ZoneParseError{ + NoCaptures, + ParseInt(core::num::ParseIntError) +} +impl std::str::FromStr for Zone{ + type Err=ZoneParseError; + fn from_str(s:&str)->Result{ + match s{ + "MapStart"=>Ok(Self::Start(ModeID::MAIN)), + "MapFinish"=>Ok(Self::Finish(ModeID::MAIN)), + "BonusStart"=>Ok(Self::Start(ModeID::BONUS)), + "BonusFinish"=>Ok(Self::Start(ModeID::BONUS)), + other=>{ + let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$"); + if let Some(captures)=bonus_start_pattern.captures(other){ + return Ok(Self::Start(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?))); + } + let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$"); + if let Some(captures)=bonus_finish_pattern.captures(other){ + return Ok(Self::Finish(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?))); + } + Err(ZoneParseError::NoCaptures) + } + } + } +} + + +#[derive(Debug,Hash,Eq,PartialEq)] +struct SpawnID(u64); +impl SpawnID{ + const FIRST:Self=Self(1); +} +#[derive(Debug,Hash,Eq,PartialEq)] +struct WormholeOutID(u64); + +struct Counts{ + mode_start_counts:std::collections::HashMap, + mode_finish_counts:std::collections::HashMap, + spawn_counts:std::collections::HashMap, + wormhole_out_counts:std::collections::HashMap, +} +impl Counts{ + fn new()->Self{ + Self{ + mode_start_counts:std::collections::HashMap::new(), + mode_finish_counts:std::collections::HashMap::new(), + spawn_counts:std::collections::HashMap::new(), + wormhole_out_counts:std::collections::HashMap::new(), + } + } +} + +pub fn check(dom:&rbx_dom_weak::WeakDom)->CheckReport{ + // empty report with all checks failed + let mut report=CheckReport::default(); + + // extract the root instance, otherwise immediately return + let Ok(model_instance)=get_root_instance(&dom)else{ + return report; + }; + + report.exactly_one_root=true; + + if model_instance.class=="Model"{ + report.root_is_model=true; + } + if model_instance.name==model_instance.name.to_snake_case(){ + report.model_name_is_snake_case=true; + } + + // extract model info + let MapInfo{display_name,creator,game_id}=get_mapinfo(&dom,model_instance); + + // check DisplayName + if let Ok(display_name)=display_name{ + if !display_name.is_empty(){ + report.has_display_name=true; + if display_name==display_name.to_title_case(){ + report.display_name_is_title_case=true; + } + } + } + + // check Creator + if let Ok(creator)=creator{ + if !creator.is_empty(){ + report.has_creator=true; + } + } + + // check GameID + if game_id.is_ok(){ + report.model_name_prefix_is_valid=true; + } + + // === MODE CHECKS === + // count objects + let mut counts=Counts::new(); + for instance in dom.descendants_of(model_instance.referent()){ + if class_is_a(instance.class.as_str(),"BasePart"){ + // Zones + match instance.name.parse(){ + Ok(Zone::Start(mode_id))=>*counts.mode_start_counts.entry(mode_id).or_insert(0)+=1, + Ok(Zone::Finish(mode_id))=>*counts.mode_finish_counts.entry(mode_id).or_insert(0)+=1, + _=>(), + } + // Spawns + let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$"); + if let Some(captures)=spawn_pattern.captures(instance.name.as_str()){ + if let Ok(spawn_id)=captures[1].parse(){ + *counts.spawn_counts.entry(SpawnID(spawn_id)).or_insert(0)+=1; + } + } + // WormholeOuts + let wormhole_out_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$"); + if let Some(captures)=wormhole_out_pattern.captures(instance.name.as_str()){ + if let Ok(wormhole_out_id)=captures[1].parse(){ + *counts.wormhole_out_counts.entry(WormholeOutID(wormhole_out_id)).or_insert(0)+=1; + } + } + } + } + + // MapStart must exist && there must be exactly one of any bonus start zones. + if counts.mode_start_counts.get(&ModeID::MAIN)==Some(&1) + &&counts.mode_start_counts.iter().all(|(_,&c)|c==1){ + report.exactly_one_mapstart=true; + } + // iterate over start zones + if counts.mode_start_counts.iter().all(|(mode_id,_)| + // ensure that at least one end zone exists with the same mode id + counts.mode_finish_counts.get(mode_id).is_some_and(|&num|0Result{ + // discover asset creator and latest version + let info=self.cloud_context.get_asset_info( + rbx_asset::cloud::GetAssetLatestRequest{asset_id:check_info.ModelID} + ).await.map_err(Error::ModelInfoDownload)?; + + // reject models created by a group + let rbx_asset::cloud::Creator::userId(_user_id_string)=info.creationContext.creator else{ + return Err(Error::CreatorTypeMustBeUser); + }; + + // parse model version string + let version=info.revisionId.parse().map_err(Error::ParseModelVersion)?; + + let model_data=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ + asset_id:check_info.ModelID, + version, + }).await.map_err(Error::Download)?; + + // decode dom (slow!) + let dom=read_dom(std::io::Cursor::new(model_data)).map_err(Error::ModelFileDecode)?; + + let report=check(&dom); + + Ok(CheckReportAndVersion{report,version}) + } +} diff --git a/validation/src/check_mapfix.rs b/validation/src/check_mapfix.rs new file mode 100644 index 0000000..b261f4e --- /dev/null +++ b/validation/src/check_mapfix.rs @@ -0,0 +1,57 @@ +use crate::check::CheckReportAndVersion; +use crate::nats_types::CheckMapfixRequest; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + Check(crate::check::Error), + ApiActionMapfixCheck(submissions_api::Error), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +impl crate::message_handler::MessageHandler{ + pub async fn check_mapfix(&self,check_info:CheckMapfixRequest)->Result<(),Error>{ + let mapfix_id=check_info.MapfixID; + let check_result=self.check_inner(check_info.into()).await; + + // update the mapfix depending on the result + match check_result{ + Ok(CheckReportAndVersion{report,version})=>{ + if report.pass(){ + // update the mapfix model status to submitted + self.api.action_mapfix_submitted( + submissions_api::types::ActionMapfixSubmittedRequest{ + MapfixID:mapfix_id, + ModelVersion:version, + } + ).await.map_err(Error::ApiActionMapfixCheck)?; + }else{ + // update the mapfix model status to request changes + self.api.action_mapfix_request_changes( + submissions_api::types::ActionMapfixRequestChangesRequest{ + MapfixID:mapfix_id, + StatusMessage:report.to_string(), + } + ).await.map_err(Error::ApiActionMapfixCheck)?; + } + }, + Err(e)=>{ + // TODO: report the error + // update the mapfix model status to request changes + self.api.action_mapfix_request_changes( + submissions_api::types::ActionMapfixRequestChangesRequest{ + MapfixID:mapfix_id, + StatusMessage:e.to_string(), + } + ).await.map_err(Error::ApiActionMapfixCheck)?; + }, + } + + Ok(()) + } +} diff --git a/validation/src/check_submission.rs b/validation/src/check_submission.rs new file mode 100644 index 0000000..12f18e1 --- /dev/null +++ b/validation/src/check_submission.rs @@ -0,0 +1,57 @@ +use crate::check::CheckReportAndVersion; +use crate::nats_types::CheckSubmissionRequest; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + Check(crate::check::Error), + ApiActionSubmissionCheck(submissions_api::Error), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +impl crate::message_handler::MessageHandler{ + pub async fn check_submission(&self,check_info:CheckSubmissionRequest)->Result<(),Error>{ + let submission_id=check_info.SubmissionID; + let check_result=self.check_inner(check_info.into()).await; + + // update the submission depending on the result + match check_result{ + Ok(CheckReportAndVersion{report,version})=>{ + if report.pass(){ + // update the submission model status to submitted + self.api.action_submission_submitted( + submissions_api::types::ActionSubmissionSubmittedRequest{ + SubmissionID:submission_id, + ModelVersion:version, + } + ).await.map_err(Error::ApiActionSubmissionCheck)?; + }else{ + // update the submission model status to request changes + self.api.action_submission_request_changes( + submissions_api::types::ActionSubmissionRequestChangesRequest{ + SubmissionID:submission_id, + StatusMessage:report.to_string(), + } + ).await.map_err(Error::ApiActionSubmissionCheck)?; + } + }, + Err(e)=>{ + // TODO: report the error + // update the submission model status to request changes + self.api.action_submission_request_changes( + submissions_api::types::ActionSubmissionRequestChangesRequest{ + SubmissionID:submission_id, + StatusMessage:e.to_string(), + } + ).await.map_err(Error::ApiActionSubmissionCheck)?; + }, + } + + Ok(()) + } +} diff --git a/validation/src/create.rs b/validation/src/create.rs index 8c99f54..9db62a8 100644 --- a/validation/src/create.rs +++ b/validation/src/create.rs @@ -1,17 +1,16 @@ -use crate::rbx_util::{get_mapinfo,read_dom,MapInfo,ReadDomError,GetMapInfoError,ParseGameIDError}; +use crate::download::download_asset_version; +use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,ParseGameIDError}; #[allow(dead_code)] #[derive(Debug)] pub enum Error{ CreatorTypeMustBeUser, ModelInfoDownload(rbx_asset::cloud::GetError), - ModelLocationDownload(rbx_asset::cloud::GetError), - NonFreeModel, - ModelFileDownload(rbx_asset::cloud::GetError), ParseUserID(core::num::ParseIntError), ParseModelVersion(core::num::ParseIntError), + Download(crate::download::Error), ModelFileDecode(ReadDomError), - GetMapInfo(GetMapInfoError), + GetRootInstance(GetRootInstanceError), ParseGameID(ParseGameIDError), } impl std::fmt::Display for Error{ @@ -49,29 +48,24 @@ impl crate::message_handler::MessageHandler{ let user_id:u64=user_id_string.parse().map_err(Error::ParseUserID)?; let asset_version=info.revisionId.parse().map_err(Error::ParseModelVersion)?; - // download the location of the map model - let location=self.cloud_context.get_asset_version_location(rbx_asset::cloud::GetAssetVersionRequest{ + // download the map model + let model_data=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ asset_id:create_info.ModelID, version:asset_version, - }).await.map_err(Error::ModelLocationDownload)?; - - // if the location does not exist, you are not allowed to donwload it - let Some(location)=location.location else{ - return Err(Error::NonFreeModel); - }; - - // download the map model - let model_data=self.cloud_context.get_asset(&location).await.map_err(Error::ModelFileDownload)?; + }).await.map_err(Error::Download)?; // decode dom (slow!) let dom=read_dom(std::io::Cursor::new(model_data)).map_err(Error::ModelFileDecode)?; + // extract the root instance + let model_instance=get_root_instance(&dom).map_err(Error::GetRootInstance)?; + // parse create fields out of asset let MapInfo{ display_name, creator, game_id, - }=get_mapinfo(&dom).map_err(Error::GetMapInfo)?; + }=get_mapinfo(&dom,model_instance); let game_id=game_id.map_err(Error::ParseGameID)?; diff --git a/validation/src/download.rs b/validation/src/download.rs new file mode 100644 index 0000000..202be72 --- /dev/null +++ b/validation/src/download.rs @@ -0,0 +1,26 @@ +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + ModelLocationDownload(rbx_asset::cloud::GetError), + NonFreeModel, + ModelFileDownload(rbx_asset::cloud::GetError), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +pub async fn download_asset_version(cloud_context:&rbx_asset::cloud::Context,request:rbx_asset::cloud::GetAssetVersionRequest)->Result,Error>{ + // download the location of the map model + let location=cloud_context.get_asset_version_location(request).await.map_err(Error::ModelLocationDownload)?; + + // if the location does not exist, you are not allowed to download it + let location=location.location.ok_or(Error::NonFreeModel)?; + + // download the map model + let model_data=cloud_context.get_asset(&location).await.map_err(Error::ModelFileDownload)?; + + Ok(model_data) +} diff --git a/validation/src/main.rs b/validation/src/main.rs index 00fb7f3..801ea3f 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -4,6 +4,10 @@ mod rbx_util; mod message_handler; mod nats_types; mod types; +mod download; +mod check; +mod check_mapfix; +mod check_submission; mod create; mod create_mapfix; mod create_submission; diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 43a6a6d..fc30782 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -7,6 +7,8 @@ pub enum HandleMessageError{ UnknownSubject(String), CreateMapfix(submissions_api::Error), CreateSubmission(submissions_api::Error), + CheckMapfix(crate::check_mapfix::Error), + CheckSubmission(crate::check_submission::Error), UploadMapfix(crate::upload_mapfix::Error), UploadSubmission(crate::upload_submission::Error), ValidateMapfix(crate::validate_mapfix::Error), @@ -52,6 +54,8 @@ impl MessageHandler{ match message.subject.as_str(){ "maptest.mapfixes.create"=>self.create_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::CreateMapfix), "maptest.submissions.create"=>self.create_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::CreateSubmission), + "maptest.mapfixes.check"=>self.check_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::CheckMapfix), + "maptest.submissions.check"=>self.check_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::CheckSubmission), "maptest.mapfixes.upload"=>self.upload_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::UploadMapfix), "maptest.submissions.upload"=>self.upload_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::UploadSubmission), "maptest.mapfixes.validate"=>self.validate_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::ValidateMapfix), diff --git a/validation/src/nats_types.rs b/validation/src/nats_types.rs index 1401f83..a1126ae 100644 --- a/validation/src/nats_types.rs +++ b/validation/src/nats_types.rs @@ -20,6 +20,20 @@ pub struct CreateMapfixRequest{ pub TargetAssetID:u64, } +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct CheckSubmissionRequest{ + pub SubmissionID:i64, + pub ModelID:u64, +} + +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct CheckMapfixRequest{ + pub MapfixID:i64, + pub ModelID:u64, +} + #[allow(nonstandard_style)] #[derive(serde::Deserialize)] pub struct ValidateSubmissionRequest{ diff --git a/validation/src/rbx_util.rs b/validation/src/rbx_util.rs index 94f9344..4d49cd0 100644 --- a/validation/src/rbx_util.rs +++ b/validation/src/rbx_util.rs @@ -100,20 +100,24 @@ fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringVal } #[derive(Debug)] -pub enum GetMapInfoError{ +pub enum GetRootInstanceError{ ModelFileRootMustHaveOneChild, ModelFileChildRefIsNil, } -pub fn get_mapinfo(dom:&rbx_dom_weak::WeakDom)->Result{ +pub fn get_root_instance(dom:&rbx_dom_weak::WeakDom)->Result<&rbx_dom_weak::Instance,GetRootInstanceError>{ let &[map_ref]=dom.root().children()else{ - return Err(GetMapInfoError::ModelFileRootMustHaveOneChild); + return Err(GetRootInstanceError::ModelFileRootMustHaveOneChild); }; - let model_instance=dom.get_by_ref(map_ref).ok_or(GetMapInfoError::ModelFileChildRefIsNil)?; + let model_instance=dom.get_by_ref(map_ref).ok_or(GetRootInstanceError::ModelFileChildRefIsNil)?; - Ok(MapInfo{ + Ok(model_instance) +} + +pub fn get_mapinfo<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&rbx_dom_weak::Instance)->MapInfo<'a>{ + MapInfo{ display_name:string_value(find_first_child_class(dom,model_instance,"DisplayName","StringValue")), creator:string_value(find_first_child_class(dom,model_instance,"Creator","StringValue")), game_id:model_instance.name.parse(), - }) + } } diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index 8df288a..b74e562 100644 --- a/validation/src/upload_mapfix.rs +++ b/validation/src/upload_mapfix.rs @@ -1,11 +1,10 @@ +use crate::download::download_asset_version; use crate::nats_types::UploadMapfixRequest; #[allow(dead_code)] #[derive(Debug)] pub enum Error{ - GetLocation(rbx_asset::cloud::GetError), - NonFreeModel, - Get(rbx_asset::cloud::GetError), + Download(crate::download::Error), Json(serde_json::Error), Upload(rbx_asset::cookie::UploadError), ApiActionMapfixUploaded(submissions_api::Error), @@ -19,19 +18,11 @@ impl std::error::Error for Error{} impl crate::message_handler::MessageHandler{ pub async fn upload_mapfix(&self,upload_info:UploadMapfixRequest)->Result<(),Error>{ - // download the location of the map model - let location=self.cloud_context.get_asset_version_location(rbx_asset::cloud::GetAssetVersionRequest{ + // download the map model + let model_data=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ asset_id:upload_info.ModelID, version:upload_info.ModelVersion, - }).await.map_err(Error::GetLocation)?; - - // if the location does not exist, you are not allowed to donwload it - let Some(location)=location.location else{ - return Err(Error::NonFreeModel); - }; - - // download the map model - let model_data=self.cloud_context.get_asset(&location).await.map_err(Error::Get)?; + }).await.map_err(Error::Download)?; // upload the map to the strafesnet group let _upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{ diff --git a/validation/src/upload_submission.rs b/validation/src/upload_submission.rs index 9b9cc50..9052433 100644 --- a/validation/src/upload_submission.rs +++ b/validation/src/upload_submission.rs @@ -1,11 +1,10 @@ +use crate::download::download_asset_version; use crate::nats_types::UploadSubmissionRequest; #[allow(dead_code)] #[derive(Debug)] pub enum Error{ - GetLocation(rbx_asset::cloud::GetError), - NonFreeModel, - Get(rbx_asset::cloud::GetError), + Download(crate::download::Error), Json(serde_json::Error), Create(rbx_asset::cookie::CreateError), SystemTime(std::time::SystemTimeError), @@ -20,19 +19,11 @@ impl std::error::Error for Error{} impl crate::message_handler::MessageHandler{ pub async fn upload_submission(&self,upload_info:UploadSubmissionRequest)->Result<(),Error>{ - // download the location of the map model - let location=self.cloud_context.get_asset_version_location(rbx_asset::cloud::GetAssetVersionRequest{ + // download the map model + let model_data=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ asset_id:upload_info.ModelID, version:upload_info.ModelVersion, - }).await.map_err(Error::GetLocation)?; - - // if the location does not exist, you are not allowed to donwload it - let Some(location)=location.location else{ - return Err(Error::NonFreeModel); - }; - - // download the map model - let model_data=self.cloud_context.get_asset(&location).await.map_err(Error::Get)?; + }).await.map_err(Error::Download)?; // upload the map to the strafesnet group let upload_response=self.cookie_context.create(rbx_asset::cookie::CreateRequest{ diff --git a/validation/src/validator.rs b/validation/src/validator.rs index 1a980e9..1215ee4 100644 --- a/validation/src/validator.rs +++ b/validation/src/validator.rs @@ -1,6 +1,7 @@ use futures::TryStreamExt; use submissions_api::types::ResourceType; +use crate::download::download_asset_version; use crate::rbx_util::{class_is_a,read_dom,ReadDomError}; use crate::types::ResourceID; @@ -36,9 +37,7 @@ pub enum Error{ ScriptFlaggedIllegalKeyword(String), ScriptBlocked(Option), ScriptNotYetReviewed(Option), - ModelLocationDownload(rbx_asset::cloud::GetError), - NonFreeModel, - ModelFileDownload(rbx_asset::cloud::GetError), + Download(crate::download::Error), ModelFileDecode(ReadDomError), ApiGetScriptPolicyFromHash(submissions_api::types::SingleItemError), ApiGetScript(submissions_api::Error), @@ -91,19 +90,11 @@ impl From for ValidateRequest{ impl crate::message_handler::MessageHandler{ pub async fn validate_inner(&self,validate_info:ValidateRequest)->Result<(),Error>{ - // download the location of the map model - let location=self.cloud_context.get_asset_version_location(rbx_asset::cloud::GetAssetVersionRequest{ + // download the map model + let model_data=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ asset_id:validate_info.ModelID, version:validate_info.ModelVersion, - }).await.map_err(Error::ModelLocationDownload)?; - - // if the location does not exist, you are not allowed to donwload it - let Some(location)=location.location else{ - return Err(Error::NonFreeModel); - }; - - // download the map model - let model_data=self.cloud_context.get_asset(&location).await.map_err(Error::ModelFileDownload)?; + }).await.map_err(Error::Download)?; // decode dom (slow!) let mut dom=read_dom(std::io::Cursor::new(model_data)).map_err(Error::ModelFileDecode)?; diff --git a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx index c9138c0..b695f45 100644 --- a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx +++ b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx @@ -10,6 +10,7 @@ interface ReviewAction { const ReviewActions = { Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, + ResetSubmitting: {name:"Reset Submitting (fix softlocked status)",action:"reset-submitting"} as ReviewAction, Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, Reject: {name:"Reject",action:"reject"} as ReviewAction, @@ -112,6 +113,9 @@ export default function ReviewButtons(props: ReviewId) { if ([MapfixStatus.Submitted, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) { visibleButtons.push({ action: ReviewActions.Revoke, color: "info", mapfixId }); } + if (mapfixStatus === MapfixStatus.Submitting) { + visibleButtons.push({ action: ReviewActions.ResetSubmitting, color: "error", mapfixId }); + } } if (roles&RolesConstants.MapfixReview) { diff --git a/web/src/app/submissions/[submissionId]/_reviewButtons.tsx b/web/src/app/submissions/[submissionId]/_reviewButtons.tsx index aa68897..2ac0ad8 100644 --- a/web/src/app/submissions/[submissionId]/_reviewButtons.tsx +++ b/web/src/app/submissions/[submissionId]/_reviewButtons.tsx @@ -10,6 +10,7 @@ interface ReviewAction { const ReviewActions = { Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, + ResetSubmitting: {name:"Reset Submitting (fix softlocked status)",action:"reset-submitting"} as ReviewAction, Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, Reject: {name:"Reject",action:"reject"} as ReviewAction, @@ -112,6 +113,9 @@ export default function ReviewButtons(props: ReviewId) { if ([SubmissionStatus.Submitted, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) { visibleButtons.push({ action: ReviewActions.Revoke, color: "info", submissionId }); } + if (submissionStatus === SubmissionStatus.Submitting) { + visibleButtons.push({ action: ReviewActions.ResetSubmitting, color: "error", submissionId }); + } } if (roles&RolesConstants.SubmissionReview) {