Unify naming of schema registering functions

This commit is contained in:
Andrey Antukh 2024-06-05 13:00:30 +02:00
parent 2921b62b37
commit d2bedec59c
24 changed files with 119 additions and 123 deletions

View file

@ -47,7 +47,7 @@
(s/keys :req-un [::path]
:opt-un [::mtype]))
(sm/def! ::fs/path
(sm/register! ::fs/path
{:type ::fs/path
:pred fs/path?
:type-properties
@ -59,7 +59,7 @@
::oapi/format "unix-path"
::oapi/decode fs/path}})
(sm/def! ::upload
(sm/register! ::upload
[:map {:title "Upload"}
[:filename :string]
[:size :int]

View file

@ -12,7 +12,7 @@
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
(sm/def! ::permissions
(sm/register! ::permissions
[:map {:title "Permissions"}
[:type {:gen/elements [:membership :share-link]} :keyword]
[:is-owner :boolean]

View file

@ -368,7 +368,7 @@
(let [p1 (System/nanoTime)]
#(duration {:nanos (- (System/nanoTime) p1)})))
(sm/def! ::instant
(sm/register! ::instant
{:type ::instant
:pred instant?
:type-properties
@ -379,7 +379,7 @@
::oapi/type "string"
::oapi/format "iso"}})
(sm/def! ::duration
(sm/register! ::duration
{:type :durations
:pred duration?
:type-properties

View file

@ -84,7 +84,7 @@
"plugins/runtime"}
(into frontend-only-features)))
(sm/def! ::features
(sm/register! ::features
[:schema
{:title "FileFeatures"
::smdj/inline true

View file

@ -33,25 +33,24 @@
(def ^:private
schema:operation
(sm/define
[:multi {:dispatch :type :title "Operation" ::smd/simplified true}
[:set
[:map {:title "SetOperation"}
[:type [:= :set]]
[:attr :keyword]
[:val :any]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set-touched
[:map {:title "SetTouchedOperation"}
[:type [:= :set-touched]]
[:touched [:maybe [:set :keyword]]]]]
[:set-remote-synced
[:map {:title "SetRemoteSyncedOperation"}
[:type [:= :set-remote-synced]]
[:remote-synced {:optional true} [:maybe :boolean]]]]]))
[:multi {:dispatch :type :title "Operation" ::smd/simplified true}
[:set
[:map {:title "SetOperation"}
[:type [:= :set]]
[:attr :keyword]
[:val :any]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set-touched
[:map {:title "SetTouchedOperation"}
[:type [:= :set-touched]]
[:touched [:maybe [:set :keyword]]]]]
[:set-remote-synced
[:map {:title "SetRemoteSyncedOperation"}
[:type [:= :set-remote-synced]]
[:remote-synced {:optional true} [:maybe :boolean]]]]])
(sm/define! ::change
(sm/register! ::change
[:schema
[:multi {:dispatch :type :title "Change" ::smd/simplified true}
[:set-option
@ -246,7 +245,7 @@
[:type [:= :del-typography]]
[:id ::sm/uuid]]]]])
(sm/define! ::changes
(sm/register! ::changes
[:sequential {:gen/max 2} ::change])
(def check-change!

View file

@ -24,7 +24,7 @@
;; Auxiliary functions to help create a set of changes (undo + redo)
(sm/define! ::changes
(sm/register! ::changes
[:map {:title "changes"}
[:redo-changes vector?]
[:undo-changes seq?]

View file

@ -90,7 +90,7 @@
(sm/lazy-validator
[:and [:fn matrix?] schema:matrix-attrs]))
(sm/def! ::matrix
(sm/register! ::matrix
(letfn [(decode [o]
(if (map? o)
(map->Matrix o)

View file

@ -61,7 +61,7 @@
(sm/lazy-validator
[:and [:fn point?] schema:point-attrs]))
(sm/def! ::point
(sm/register! ::point
(letfn [(decode [p]
(if (map? p)
(map->Point p)

View file

@ -80,7 +80,7 @@
[:x2 ::sm/safe-number]
[:y2 ::sm/safe-number]])
(sm/define! ::rect
(sm/register! ::rect
[:and
{:gen/gen (->> (sg/tuple (sg/small-double)
(sg/small-double)

View file

@ -75,7 +75,8 @@
(-explain s value)
(m/explain s value default-options)))
(defn humanize
(defn simplify
"Given an explain data structure, return a simplified version of it"
[exp]
(me/humanize exp))
@ -86,10 +87,12 @@
(mg/generate (schema s) o)))
(defn form
"Returns a readable form of the schema"
[s]
(m/form s default-options))
(defn merge
"Merge two schemas"
[& items]
(apply mu/merge (map schema items)))
@ -102,6 +105,7 @@
(m/deref s))
(defn error-values
"Get error values form explain data structure"
[exp]
(malli.error/error-value exp {:malli.error/mask-valid-values '...}))
@ -138,18 +142,6 @@
:decoders coders
:encoders coders})))
(defn validator
[s]
(if (lazy-schema? s)
(-get-validator s)
(-> s schema m/validator)))
(defn explainer
[s]
(if (lazy-schema? s)
(-get-explainer s)
(-> s schema m/explainer)))
(defn encode
([s val transformer]
(m/encode s val default-options transformer))
@ -164,6 +156,18 @@
([s val options transformer]
(m/decode s val options transformer)))
(defn validator
[s]
(if (lazy-schema? s)
(-get-validator s)
(-> s schema m/validator)))
(defn explainer
[s]
(if (lazy-schema? s)
(-get-explainer s)
(-> s schema m/explainer)))
(defn encoder
([s]
(if (lazy-schema? s)
@ -201,6 +205,7 @@
(fn [v] (@vfn v)))))
(defn humanize-explain
"Returns a string representation of the explain data structure"
[{:keys [schema errors value]} & {:keys [length level]}]
(let [errors (mapv #(update % :schema form) errors)]
(with-out-str
@ -213,7 +218,6 @@
:level (d/nilv level 8)
:length (d/nilv length 12)})))))
(defmethod v/-format ::schemaless-explain
[_ {:keys [schema] :as explanation} printer]
{:body [:group
@ -353,15 +357,8 @@
(defn register! [type s]
(let [s (if (map? s) (simple-schema s) s)]
(swap! sr/registry assoc type s)))
(defn def! [type s]
(register! type s)
nil)
(defn define! [id s]
(register! id s)
nil)
(swap! sr/registry assoc type s)
nil))
(defn define
"Create ans instance of ILazySchema"
@ -435,8 +432,8 @@
;; --- BUILTIN SCHEMAS
(define! :merge (mu/-merge))
(define! :union (mu/-union))
(register! :merge (mu/-merge))
(register! :union (mu/-union))
(def uuid-rx
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
@ -447,7 +444,7 @@
(some->> (re-matches uuid-rx s) uuid/uuid)
s))
(define! ::uuid
(register! ::uuid
{:type ::uuid
:pred uuid?
:type-properties
@ -472,7 +469,7 @@
(and (string? s)
(re-seq email-re s)))
(define! ::email
(register! ::email
{:type :string
:pred email-string?
:property-pred
@ -501,7 +498,7 @@
;; NOTE: this is general purpose set spec and should be used over the other
(define! ::set
(register! ::set
{:type :set
:min 0
:max 1
@ -557,7 +554,7 @@
(into #{} xform v)))}}))})
(define! ::vec
(register! ::vec
{:type :vector
:min 0
:max 1
@ -614,7 +611,7 @@
(into [] xform v)))}}))})
(define! ::set-of-strings
(register! ::set-of-strings
{:type ::set-of-strings
:pred #(and (set? %) (every? string? %))
:type-properties
@ -630,7 +627,7 @@
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} non-empty-strings-xf v)))}})
(define! ::set-of-keywords
(register! ::set-of-keywords
{:type ::set-of-keywords
:pred #(and (set? %) (every? keyword? %))
:type-properties
@ -646,7 +643,7 @@
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (comp non-empty-strings-xf (map keyword)) v)))}})
(define! ::set-of-emails
(register! ::set-of-emails
{:type ::set-of-emails
:pred #(and (set? %) (every? string? %))
:type-properties
@ -662,7 +659,7 @@
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (keep parse-email) v)))}})
(define! ::set-of-uuid
(register! ::set-of-uuid
{:type ::set-of-uuid
:pred #(and (set? %) (every? uuid? %))
:type-properties
@ -678,7 +675,7 @@
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (keep parse-uuid) v)))}})
(define! ::coll-of-uuid
(register! ::coll-of-uuid
{:type ::set-of-uuid
:pred (partial every? uuid?)
:type-properties
@ -694,7 +691,7 @@
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into [] (keep parse-uuid) v)))}})
(define! ::one-of
(register! ::one-of
{:type ::one-of
:min 1
:max 1
@ -717,7 +714,7 @@
;; Integer/MIN_VALUE
(def min-safe-int -2147483648)
(define! ::safe-int
(register! ::safe-int
{:type ::safe-int
:pred #(and (int? %) (>= max-safe-int %) (>= % min-safe-int))
:type-properties
@ -732,7 +729,7 @@
(parse-long s)
s))}})
(define! ::safe-number
(register! ::safe-number
{:type ::safe-number
:pred #(and (number? %) (>= max-safe-int %) (>= % min-safe-int))
:type-properties
@ -748,7 +745,7 @@
(parse-double s)
s))}})
(define! ::safe-double
(register! ::safe-double
{:type ::safe-double
:pred #(and (double? %) (>= max-safe-int %) (>= % min-safe-int))
:type-properties
@ -763,7 +760,7 @@
(parse-double s)
s))}})
(define! ::contains-any
(register! ::contains-any
{:type ::contains-any
:min 1
:max 1
@ -781,7 +778,7 @@
{:title "contains"
:description "contains predicate"}}))})
(define! ::inst
(register! ::inst
{:type ::inst
:pred inst?
:type-properties
@ -793,12 +790,12 @@
::oapi/type "number"
::oapi/format "int64"}})
(define! ::fn
(register! ::fn
[:schema fn?])
;; FIXME: deprecated, replace with ::text
(define! ::word-string
(register! ::word-string
{:type ::word-string
:pred #(and (string? %) (not (str/blank? %)))
:property-pred (m/-min-max-pred count)
@ -810,7 +807,7 @@
::oapi/type "string"
::oapi/format "string"}})
(define! ::uri
(register! ::uri
{:type ::uri
:pred u/uri?
:property-pred
@ -849,7 +846,7 @@
::oapi/format "uri"
::oapi/decode (comp u/uri str/trim)}})
(define! ::text
(register! ::text
{:type :string
:pred #(and (string? %) (not (str/blank? %)))
:property-pred
@ -891,7 +888,7 @@
(str/blank? value))
"errors.field-not-all-whitespace")))}})
(define! ::password
(register! ::password
{:type :string
:pred
(fn [value]
@ -908,7 +905,7 @@
;; FIXME: this should not be here
(define! ::plugin-data
(register! ::plugin-data
[:map-of {:gen/max 5} :string :string])
;; ---- PREDICATES

View file

@ -37,7 +37,7 @@
(.. g (toString 16) (padStart 2 "0"))
(.. b (toString 16) (padStart 2 "0"))))))
(sm/define! ::rgb-color
(sm/register! ::rgb-color
{:type ::rgb-color
:pred #(and (string? %) (some? (re-matches rgb-color-re %)))
:type-properties
@ -49,7 +49,7 @@
::oapi/type "integer"
::oapi/format "int64"}})
(sm/define! ::image-color
(sm/register! ::image-color
[:map {:title "ImageColor"}
[:name {:optional true} :string]
[:width :int]
@ -58,7 +58,7 @@
[:id ::sm/uuid]
[:keep-aspect-ratio {:optional true} :boolean]])
(sm/define! ::gradient
(sm/register! ::gradient
[:map {:title "Gradient"}
[:type [::sm/one-of #{:linear :radial}]]
[:start-x ::sm/safe-number]
@ -73,7 +73,7 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:offset ::sm/safe-number]]]]])
(sm/define! ::color
(sm/register! ::color
[:and
[:map {:title "Color"}
[:id {:optional true} ::sm/uuid]
@ -91,7 +91,7 @@
[:map-of {:gen/max 5} :keyword ::sm/plugin-data]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/define! ::recent-color
(sm/register! ::recent-color
[:and
[:map {:title "RecentColor"}
[:opacity {:optional true} [:maybe ::sm/safe-number]]

View file

@ -26,7 +26,7 @@
(def valid-container-types
#{:page :component})
(sm/define! ::container
(sm/register! ::container
[:map
[:id ::sm/uuid]
[:type {:optional true}

View file

@ -34,7 +34,7 @@
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/define! ::media-object
(sm/register! ::media-object
[:map {:title "FileMediaObject"}
[:id ::sm/uuid]
[:name :string]
@ -43,7 +43,7 @@
[:mtype :string]
[:path {:optional true} [:maybe :string]]])
(sm/define! ::data
(sm/register! ::data
[:map {:title "FileData"}
[:pages [:vector ::sm/uuid]]
[:pages-index

View file

@ -13,12 +13,12 @@
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/def! ::grid-color
(sm/register! ::grid-color
[:map {:title "PageGridColor"}
[:color ::ctc/rgb-color]
[:opacity ::sm/safe-number]])
(sm/def! ::column-params
(sm/register! ::column-params
[:map
[:color ::grid-color]
[:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]]
@ -27,12 +27,12 @@
[:item-length {:optional true} [:maybe ::sm/safe-number]]
[:gutter {:optional true} [:maybe ::sm/safe-number]]])
(sm/def! ::square-params
(sm/register! ::square-params
[:map
[:size {:optional true} [:maybe ::sm/safe-number]]
[:color ::grid-color]])
(sm/def! ::grid
(sm/register! ::grid
[:multi {:dispatch :type}
[:column
[:map
@ -52,7 +52,7 @@
[:display :boolean]
[:params ::square-params]]]])
(sm/def! ::saved-grids
(sm/register! ::saved-grids
[:map {:title "PageGrid"}
[:square {:optional true} ::square-params]
[:row {:optional true} ::column-params]

View file

@ -17,20 +17,20 @@
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/define! ::flow
(sm/register! ::flow
[:map {:title "PageFlow"}
[:id ::sm/uuid]
[:name :string]
[:starting-frame ::sm/uuid]])
(sm/define! ::guide
(sm/register! ::guide
[:map {:title "PageGuide"}
[:id ::sm/uuid]
[:axis [::sm/one-of #{:x :y}]]
[:position ::sm/safe-number]
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
(sm/define! ::page
(sm/register! ::page
[:map {:title "FilePage"}
[:id ::sm/uuid]
[:name :string]

View file

@ -85,10 +85,10 @@
:exclude
:intersection})
(sm/define! ::points
(sm/register! ::points
[:vector {:gen/max 4 :gen/min 4} ::gpt/point])
(sm/define! ::fill
(sm/register! ::fill
[:map {:title "Fill"}
[:fill-color {:optional true} ::ctc/rgb-color]
[:fill-opacity {:optional true} ::sm/safe-number]
@ -97,7 +97,7 @@
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]
[:fill-image {:optional true} ::ctc/image-color]])
(sm/define! ::stroke
(sm/register! ::stroke
[:map {:title "Stroke"}
[:stroke-color {:optional true} :string]
[:stroke-color-ref-file {:optional true} ::sm/uuid]
@ -115,7 +115,7 @@
[:stroke-color-gradient {:optional true} ::ctc/gradient]
[:stroke-image {:optional true} ::ctc/image-color]])
(sm/define! ::shape-base-attrs
(sm/register! ::shape-base-attrs
[:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid]
[:name :string]
@ -127,14 +127,14 @@
[:parent-id ::sm/uuid]
[:frame-id ::sm/uuid]])
(sm/define! ::shape-geom-attrs
(sm/register! ::shape-geom-attrs
[:map {:title "ShapeGeometryAttrs"}
[:x ::sm/safe-number]
[:y ::sm/safe-number]
[:width ::sm/safe-number]
[:height ::sm/safe-number]])
(sm/define! ::shape-attrs
(sm/register! ::shape-attrs
[:map {:title "ShapeAttrs"}
[:name {:optional true} :string]
[:component-id {:optional true} ::sm/uuid]
@ -190,12 +190,12 @@
[:plugin-data {:optional true}
[:map-of {:gen/max 5} :keyword ::sm/plugin-data]]])
(sm/define! ::group-attrs
(sm/register! ::group-attrs
[:map {:title "GroupAttrs"}
[:type [:= :group]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]])
(sm/define! ::frame-attrs
(sm/register! ::frame-attrs
[:map {:title "FrameAttrs"}
[:type [:= :frame]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
@ -203,7 +203,7 @@
[:show-content {:optional true} :boolean]
[:hide-in-viewer {:optional true} :boolean]])
(sm/define! ::bool-attrs
(sm/register! ::bool-attrs
[:map {:title "BoolAttrs"}
[:type [:= :bool]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
@ -223,19 +223,19 @@
[:maybe
[:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]])
(sm/define! ::rect-attrs
(sm/register! ::rect-attrs
[:map {:title "RectAttrs"}
[:type [:= :rect]]])
(sm/define! ::circle-attrs
(sm/register! ::circle-attrs
[:map {:title "CircleAttrs"}
[:type [:= :circle]]])
(sm/define! ::svg-raw-attrs
(sm/register! ::svg-raw-attrs
[:map {:title "SvgRawAttrs"}
[:type [:= :svg-raw]]])
(sm/define! ::image-attrs
(sm/register! ::image-attrs
[:map {:title "ImageAttrs"}
[:type [:= :image]]
[:metadata
@ -245,17 +245,17 @@
[:mtype {:optional true} [:maybe :string]]
[:id ::sm/uuid]]]])
(sm/define! ::path-attrs
(sm/register! ::path-attrs
[:map {:title "PathAttrs"}
[:type [:= :path]]
[:content ::ctsp/content]])
(sm/define! ::text-attrs
(sm/register! ::text-attrs
[:map {:title "TextAttrs"}
[:type [:= :text]]
[:content {:optional true} [:maybe ::ctsx/content]]])
(sm/define! ::shape-map
(sm/register! ::shape-map
[:multi {:dispatch :type :title "Shape"}
[:group
[:and {:title "GroupShape"}
@ -327,7 +327,7 @@
::text-attrs
::ctsl/layout-child-attrs]]])
(sm/define! ::shape
(sm/register! ::shape
[:and
{:title "Shape"
:gen/gen (->> (sg/generator ::shape-base-attrs)

View file

@ -26,7 +26,7 @@
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/def! ::blur
(sm/register! ::blur
[:map {:title "Blur"}
[:id ::sm/uuid]
[:type [:= :layer-blur]]

View file

@ -10,7 +10,7 @@
(def export-types #{:png :jpeg :svg :pdf})
(sm/def! ::export
(sm/register! ::export
[:map {:title "ShapeExport"}
[:type :keyword]
[:scale ::sm/safe-number]

View file

@ -71,7 +71,7 @@
(def animation-types
#{:dissolve :slide :push})
(sm/define! ::animation
(sm/register! ::animation
[:multi {:dispatch :animation-type :title "Animation"}
[:dissolve
[:map {:title "AnimationDisolve"}
@ -96,7 +96,7 @@
(def check-animation!
(sm/check-fn ::animation))
(sm/define! ::interaction
(sm/register! ::interaction
[:multi {:dispatch :action-type}
[:navigate
[:map

View file

@ -87,7 +87,7 @@
:layout-item-absolute
:layout-item-z-index])
(sm/def! ::layout-attrs
(sm/register! ::layout-attrs
[:map {:title "LayoutAttrs"}
[:layout {:optional true} [::sm/one-of layout-types]]
[:layout-flex-dir {:optional true} [::sm/one-of flex-direction-types]]
@ -130,7 +130,7 @@
(def grid-cell-justify-self-types
#{:auto :start :center :end :stretch})
(sm/def! ::grid-cell
(sm/register! ::grid-cell
[:map {:title "GridCell"}
[:id ::sm/uuid]
[:area-name {:optional true} :string]
@ -144,7 +144,7 @@
[:shapes
[:vector {:gen/max 1} ::sm/uuid]]])
(sm/def! ::grid-track
(sm/register! ::grid-track
[:map {:title "GridTrack"}
[:type [::sm/one-of grid-track-types]]
[:value {:optional true} [:maybe ::sm/safe-number]]])
@ -166,7 +166,7 @@
(def item-align-self-types
#{:start :end :center :stretch})
(sm/def! ::layout-child-attrs
(sm/register! ::layout-child-attrs
[:map {:title "LayoutChildAttrs"}
[:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]]
[:layout-item-margin {:optional true}
@ -192,7 +192,7 @@
(def valid-layouts
#{:flex :grid})
(sm/def! ::layout
(sm/register! ::layout
[::sm/one-of valid-layouts])
(defn flex-layout?

View file

@ -12,7 +12,7 @@
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/define! ::segment
(sm/register! ::segment
[:multi {:title "PathSegment" :dispatch :command}
[:line-to
[:map
@ -43,5 +43,5 @@
[:c2x ::sm/safe-number]
[:c2y ::sm/safe-number]]]]]])
(sm/define! ::content
(sm/register! ::content
[:vector ::segment])

View file

@ -11,7 +11,7 @@
(def styles #{:drop-shadow :inner-shadow})
(sm/def! ::shadow
(sm/register! ::shadow
[:map {:title "Shadow"}
[:id [:maybe ::sm/uuid]]
[:style [::sm/one-of styles]]

View file

@ -16,7 +16,7 @@
(def node-types #{"root" "paragraph-set" "paragraph"})
(sm/def! ::content
(sm/register! ::content
[:map
[:type [:= "root"]]
[:key {:optional true} :string]
@ -64,7 +64,7 @@
(sm/def! ::position-data
(sm/register! ::position-data
[:vector {:min 1 :gen/max 2}
[:map
[:x ::sm/safe-number]

View file

@ -15,7 +15,7 @@
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/def! ::typography
(sm/register! ::typography
[:map {:title "Typography"}
[:id ::sm/uuid]
[:name :string]