summaryrefslogtreecommitdiff
path: root/internal/api/model
diff options
context:
space:
mode:
authorLibravatar Vyr Cossont <VyrCossont@users.noreply.github.com>2024-11-26 08:23:00 -0800
committerLibravatar GitHub <noreply@github.com>2024-11-26 08:23:00 -0800
commit6a8af426474acd1ffd5cb3265a2372003977326a (patch)
tree9f22c7d3eabc784728da22d60f0ec05ed93ac6ac /internal/api/model
parent[chore] Sign the bloody thing, fix the other bloody thing (#3572) (diff)
downloadgotosocial-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.go2
-rw-r--r--internal/api/model/filterv2.go4
-rw-r--r--internal/api/model/nullable.go107
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,
+ }
+}