diff options
author | 2024-11-26 08:23:00 -0800 | |
---|---|---|
committer | 2024-11-26 08:23:00 -0800 | |
commit | 6a8af426474acd1ffd5cb3265a2372003977326a (patch) | |
tree | 9f22c7d3eabc784728da22d60f0ec05ed93ac6ac /internal/api/model | |
parent | [chore] Sign the bloody thing, fix the other bloody thing (#3572) (diff) | |
download | gotosocial-6a8af426474acd1ffd5cb3265a2372003977326a.tar.xz |
[bugfix] Allow unsetting filter expiration dates (#3560)
* Regression tests for #3497 (v1 and v2)
* use Nullable type for v2 form.expires_in
---------
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Diffstat (limited to 'internal/api/model')
-rw-r--r-- | internal/api/model/filterv1.go | 2 | ||||
-rw-r--r-- | internal/api/model/filterv2.go | 4 | ||||
-rw-r--r-- | internal/api/model/nullable.go | 107 |
3 files changed, 110 insertions, 3 deletions
diff --git a/internal/api/model/filterv1.go b/internal/api/model/filterv1.go index 1c3b5fb8e..0b092627e 100644 --- a/internal/api/model/filterv1.go +++ b/internal/api/model/filterv1.go @@ -95,5 +95,5 @@ type FilterCreateUpdateRequestV1 struct { // Number of seconds from now that the filter should expire. If omitted, filter never expires. // // Example: 86400 - ExpiresInI interface{} `json:"expires_in"` + ExpiresInI Nullable[any] `json:"expires_in"` } diff --git a/internal/api/model/filterv2.go b/internal/api/model/filterv2.go index 242c569dc..26b1b22b3 100644 --- a/internal/api/model/filterv2.go +++ b/internal/api/model/filterv2.go @@ -134,7 +134,7 @@ type FilterCreateRequestV2 struct { // Number of seconds from now that the filter should expire. If omitted, filter never expires. // // Example: 86400 - ExpiresInI interface{} `json:"expires_in"` + ExpiresInI Nullable[any] `json:"expires_in"` // Keywords to be added to the newly created filter. Keywords []FilterKeywordCreateUpdateRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"` @@ -199,7 +199,7 @@ type FilterUpdateRequestV2 struct { // Number of seconds from now that the filter should expire. If omitted, filter never expires. // // Example: 86400 - ExpiresInI interface{} `json:"expires_in"` + ExpiresInI Nullable[any] `json:"expires_in"` // Keywords to be added to the filter, modified, or removed. Keywords []FilterKeywordCreateUpdateDeleteRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"` diff --git a/internal/api/model/nullable.go b/internal/api/model/nullable.go new file mode 100644 index 000000000..4dd02f854 --- /dev/null +++ b/internal/api/model/nullable.go @@ -0,0 +1,107 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package model + +import ( + "bytes" + "encoding/json" + "errors" +) + +// Nullable is a generic type, which implements a field that can be one of three states: +// +// - field is not set in the request +// - field is explicitly set to `null` in the request +// - field is explicitly set to a valid value in the request +// +// Nullable is intended to be used with JSON unmarshalling. +// +// Adapted from https://github.com/oapi-codegen/nullable/blob/main/nullable.go +type Nullable[T any] struct { + state nullableState + value T +} + +type nullableState uint8 + +const ( + nullableStateUnspecified nullableState = 0 + nullableStateNull nullableState = 1 + nullableStateSet nullableState = 2 +) + +// Get retrieves the underlying value, if present, +// and returns an error if the value was not present. +func (t Nullable[T]) Get() (T, error) { + var empty T + if t.IsNull() { + return empty, errors.New("value is null") + } + + if !t.IsSpecified() { + return empty, errors.New("value is not specified") + } + + return t.value, nil +} + +// IsNull indicates whether the field +// was sent, and had a value of `null` +func (t Nullable[T]) IsNull() bool { + return t.state == nullableStateNull +} + +// IsSpecified indicates whether the field +// was sent either as a value or as `null`. +func (t Nullable[T]) IsSpecified() bool { + return t.state != nullableStateUnspecified +} + +// If field is unspecified, +// UnmarshalJSON won't be called. +func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + // If field is specified as `null`. + if bytes.Equal(data, []byte("null")) { + t.setNull() + return nil + } + + // Otherwise, we have an + // actual value, so parse it. + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + t.set(v) + return nil +} + +// setNull indicates that the field +// was sent, and had a value of `null` +func (t *Nullable[T]) setNull() { + *t = Nullable[T]{state: nullableStateNull} +} + +// set the underlying value to given value. +func (t *Nullable[T]) set(value T) { + *t = Nullable[T]{ + state: nullableStateSet, + value: value, + } +} |