Skip to content
Snippets Groups Projects
Unverified Commit b0147303 authored by Walter Leibbrandt's avatar Walter Leibbrandt Committed by GitHub
Browse files

Wrap `tru` and `trs` macro results in `str` (#10571)

* Add versions of `trs` and `tru` that wrap results in `str`

The pre-existing `trs` and `tru` were renamed to `lazy-trs` and `lazy-tru`
respectively, and new `trs` and `tru` funcs were added, wrapping the result
of its lazy counterpart in `str`.

This way the normal use case of `trs` and `tru` is improved by no longer
requiring that it be wrapped in `str`.

Cases where the translated result is passed to `str` anyway, the lazy
version can be used.

* `(str (trs ...))` → `(trs ...)`

And the same for `tru`.

* Allow `defsetting` descriptions to use lazy versions of `trs`/`tru`

* Use `lazy-trs`/`lazy-tru` where `str` is applied separately

* Remove unnecessary `vec` from `tru`/`trs` macros

* puppetlabs.i18n does not have a `lazy-tru`

* Missed some settings that now need to use `lazy-tru`

* Only use `lazy-tru` in top-level `def`s

If the localized strings are looked up on module import, before locales are
loaded, the lookup will fail and the original input string will be used.

* Fix bad usages of `tru` that should be `lazy-tru`

* s/lazy-tr/deferred-tr/g

* Avoid `tr*` macros being used in top-level (compile time) statements

Because localizations are not yet loaded at compile time.

* Add type hint for `str*` wrapping of `trs`/`tru`

* Fix assigned function's return type hint

* Expand docstrings for `i18n/tr[su]`

* Use `trs` and `tru` from metabase.util.i18n instead of puppetlabs.i18n.core

* Defer compile time translation lookups

These changes were pointed out by exceptions generated by `i18n/str*`.

* Remove unused `require`d function

* New compile time `trs` usages caught by `lein uberjar`

* Ensure that error message from db driver is a string

It is necessary to ensure that the error message is a string, because some
db driver errors come from statically declared error messages, using
`deferred-tru`. Like those in
`metabase.driver.common/connection-error-messages`.

Fixes #10347 and #10473.

* Remove string type hint from `humanize-connect-error-message`
parent 9a264146
No related branches found
No related tags found
No related merge requests found
Showing
with 75 additions and 75 deletions
......@@ -335,7 +335,7 @@
[honeysql-form :- su/Map]
(let [[sql & args] (sql.qp/honeysql-form->sql+args :bigquery honeysql-form)]
(when (seq args)
(throw (Exception. (str (tru "BigQuery statements can''t be parameterized!")))))
(throw (Exception. (tru "BigQuery statements can''t be parameterized!"))))
sql))
;; From the dox: Fields must contain only letters, numbers, and underscores, start with a letter or underscore, and be
......
......@@ -35,11 +35,11 @@
(:body options) (update :body json/generate-string))
{:keys [status body]} (request-fn url options)]
(when (not= status 200)
(throw (Exception. (str (tru "Error [{0}]: {1}" status body)))))
(throw (Exception. (tru "Error [{0}]: {1}" status body))))
(try
(json/parse-string body keyword)
(catch Throwable _
(throw (Exception. (str (tru "Failed to parse body: {0}" body))))))))
(throw (Exception. (tru "Failed to parse body: {0}" body)))))))
(def ^:private ^{:arglists '([url & {:as options}])} GET (partial do-request http/get))
(def ^:private ^{:arglists '([url & {:as options}])} POST (partial do-request http/post))
......
......@@ -649,7 +649,7 @@
;; we should never get here unless our code is B U S T E D
_
(throw (ex-info (str (tru "Expected :aggregation-options, constant, or expression."))
(throw (ex-info (tru "Expected :aggregation-options, constant, or expression.")
{:type :bug, :input arg})))))}))
......
......@@ -229,8 +229,8 @@
;; if we get a big long message about how we need to enable the GA API, then replace it with a short message about
;; how we need to enable the API
(if-let [[_ enable-api-url] (re-find #"Enable it by visiting ([^\s]+) then retry." message)]
(str (tru "You must enable the Google Analytics API. Use this link to go to the Google Developers Console: {0}"
enable-api-url))
(tru "You must enable the Google Analytics API. Use this link to go to the Google Developers Console: {0}"
enable-api-url)
message))
(defmethod driver/mbql->native :googleanalytics [_ query]
......
......@@ -7,7 +7,7 @@
[metabase.query-processor.store :as qp.store]
[metabase.util
[date :as du]
[i18n :as ui18n :refer [tru]]
[i18n :as ui18n :refer [deferred-tru tru]]
[schema :as su]]
[schema.core :as s])
(:import [com.google.api.services.analytics.model GaData GaData$ColumnHeaders]))
......@@ -231,12 +231,12 @@
(defn- maybe-get-only-filter-or-throw [filters]
(when-let [filters (seq (filter some? filters))]
(when (> (count filters) 1)
(throw (Exception. (str (tru "Multiple date filters are not supported")))))
(throw (Exception. (tru "Multiple date filters are not supported"))))
(first filters)))
(defn- try-reduce-filters [[filter1 filter2]]
(merge-with
(fn [_ _] (throw (Exception. (str (tru "Multiple date filters are not supported in filters: ") filter1 filter2))))
(fn [_ _] (throw (Exception. (str (deferred-tru "Multiple date filters are not supported in filters: ") filter1 filter2))))
filter1 filter2))
(defmethod parse-filter:interval :and [[_ & subclauses]]
......@@ -249,7 +249,7 @@
(maybe-get-only-filter-or-throw (map parse-filter:interval subclauses)))
(defmethod parse-filter:interval :not [[& _]]
(throw (Exception. (str (tru ":not is not yet implemented")))))
(throw (Exception. (tru ":not is not yet implemented"))))
(defn- remove-non-datetime-filter-clauses
"Replace any filter clauses that operate on a non-datetime Field with `nil`."
......@@ -295,7 +295,7 @@
[{filter-clause :filter}]
(let [segments (mbql.u/match filter-clause [:segment (segment-name :guard mbql.u/ga-id?)] segment-name)]
(when (> (count segments) 1)
(throw (Exception. (str (tru "Only one Google Analytics segment allowed at a time.")))))
(throw (Exception. (tru "Only one Google Analytics segment allowed at a time."))))
(first segments)))
(defn- handle-filter:built-in-segment
......
......@@ -21,7 +21,7 @@
[metabase.util :as u]
[metabase.util
[date :as du]
[i18n :as ui18n :refer [tru]]
[i18n :as ui18n :refer [deferred-tru tru]]
[schema :as su]]
[monger
[collection :as mc]
......@@ -90,7 +90,7 @@
(defn- log-aggregation-pipeline [form]
(when-not i/*disable-qp-logging*
(log/debug (u/format-color 'green (str "\n" (tru "MONGO AGGREGATION PIPELINE:") "\n%s\n")
(log/debug (u/format-color 'green (str "\n" (deferred-tru "MONGO AGGREGATION PIPELINE:") "\n%s\n")
(->> form
;; strip namespace qualifiers from Monger form
(walk/postwalk #(if (symbol? %) (symbol (name %)) %))
......@@ -428,7 +428,7 @@
:else
(throw
(ex-info (str (tru "Don't know how to handle aggregation {0}" ag))
(ex-info (tru "Don't know how to handle aggregation {0}" ag)
{:type :invalid-query, :clause ag}))))
(defn- unwrap-named-ag [[ag-type arg :as ag]]
......@@ -736,7 +736,7 @@
actual-cols (set (keys (first results)))
not-in-expected (set/difference actual-cols expected-cols)]
(when (seq not-in-expected)
(throw (Exception. (str (tru "Unexpected columns in results: {0}" (sort not-in-expected)))))))))
(throw (Exception. (tru "Unexpected columns in results: {0}" (sort not-in-expected))))))))
(defn execute-query
"Process and run a native MongoDB query."
......
......@@ -131,7 +131,7 @@
Docs to generate URI string: https://docs.mongodb.com/manual/reference/connection-string/#dns-seedlist-connection-format"
[{:keys [host port user authdb pass dbname ssl additional-options]}]
(if-not (fqdn? host)
(throw (ex-info (str (tru "Using DNS SRV requires a FQDN for host" ))
(throw (ex-info (tru "Using DNS SRV requires a FQDN for host")
{:host host}))
(let [conn-opts (connection-options-builder :ssl? ssl, :additional-options additional-options)
authdb (if (seq authdb)
......@@ -177,7 +177,7 @@
(let [mongo-client (MongoClient. uri)]
(if-let [db-name (.getDatabase uri)]
[mongo-client (.getDB mongo-client db-name)]
(throw (ex-info (str (tru "No database name specified in URI. Monger requires a database to be explicitly configured." ))
(throw (ex-info (tru "No database name specified in URI. Monger requires a database to be explicitly configured.")
{:hosts (-> uri .getHosts)
:uri (-> uri .getURI)
:opts (-> uri .getOptions)})))))
......
......@@ -134,7 +134,7 @@
[{details :details}]
(or (:db details)
(:dbname details)
(throw (Exception. (str (tru "Invalid Snowflake connection details: missing DB name."))))))
(throw (Exception. (tru "Invalid Snowflake connection details: missing DB name.")))))
(defn- query-db-name []
;; the store is always initialized when running QP queries; for some stuff like the test extensions DDL statements
......
......@@ -22,20 +22,20 @@
[dashboard :as transform.dashboard]
[materialize :as transform.materialize]]
[metabase.util
[i18n :refer [tru]]
[i18n :refer [deferred-tru]]
[schema :as su]]
[ring.util.codec :as codec]
[schema.core :as s]))
(def ^:private Show
(su/with-api-error-message (s/maybe (s/enum "all"))
(tru "invalid show value")))
(deferred-tru "invalid show value")))
(def ^:private Prefix
(su/with-api-error-message
(s/pred (fn [prefix]
(some #(not-empty (rules/get-rules [% prefix])) ["table" "metric" "field"])))
(tru "invalid value for prefix")))
(deferred-tru "invalid value for prefix")))
(def ^:private Rule
(su/with-api-error-message
......@@ -47,7 +47,7 @@
:rule)
(rules/get-rules [toplevel])))
["table" "metric" "field"])))
(tru "invalid value for rule name")))
(deferred-tru "invalid value for rule name")))
(def ^:private ^{:arglists '([s])} decode-base64-json
(comp #(json/decode % keyword) codecs/bytes->str codec/base64-decode))
......@@ -55,7 +55,7 @@
(def ^:private Base64EncodedJSON
(su/with-api-error-message
(s/pred decode-base64-json)
(tru "value couldn''t be parsed as base64 encoded JSON")))
(deferred-tru "value couldn''t be parsed as base64 encoded JSON")))
(api/defendpoint GET "/database/:id/candidates"
"Return a list of candidates for automagic dashboards orderd by interestingness."
......@@ -96,12 +96,12 @@
(def ^:private Entity
(su/with-api-error-message
(apply s/enum (keys ->entity))
(tru "Invalid entity type")))
(deferred-tru "Invalid entity type")))
(def ^:private ComparisonEntity
(su/with-api-error-message
(s/enum "segment" "adhoc" "table")
(tru "Invalid comparison entity type. Can only be one of \"table\", \"segment\", or \"adhoc\"")))
(deferred-tru "Invalid comparison entity type. Can only be one of \"table\", \"segment\", or \"adhoc\"")))
(api/defendpoint GET "/:entity/:entity-id-or-query"
"Return an automagic dashboard for entity `entity` with id `ìd`."
......
......@@ -11,7 +11,7 @@
[metabase.api.common.internal :refer :all]
[metabase.models.interface :as mi]
[metabase.util
[i18n :as ui18n :refer [trs tru]]
[i18n :as ui18n :refer [deferred-trs deferred-tru tru]]
[schema :as su]]
[schema.core :as s]
[toucan.db :as db]))
......@@ -144,7 +144,7 @@
;; #### GENERIC 400 RESPONSE HELPERS
(def ^:private generic-400
[400 (tru "Invalid Request.")])
[400 (deferred-tru "Invalid Request.")])
(defn check-400
"Throw a `400` if `arg` is `false` or `nil`, otherwise return as-is."
......@@ -159,7 +159,7 @@
;; #### GENERIC 404 RESPONSE HELPERS
(def ^:private generic-404
[404 (tru "Not found.")])
[404 (deferred-tru "Not found.")])
(defn check-404
"Throw a `404` if `arg` is `false` or `nil`, otherwise return as-is."
......@@ -196,7 +196,7 @@
;; #### GENERIC 500 RESPONSE HELPERS
;; For when you don't feel like writing something useful
(def ^:private generic-500
[500 (tru "Internal server error.")])
[500 (deferred-tru "Internal server error.")])
(defn check-500
"Throw a `500` if `arg` is `false` or `nil`, otherwise return as-is."
......@@ -245,7 +245,7 @@
[arg->schema body] (u/optional (every-pred map? #(every? symbol? (keys %))) more)
validate-param-calls (validate-params arg->schema)]
(when-not docstr
(log/warn (trs "Warning: endpoint {0}/{1} does not have a docstring." (ns-name *ns*) fn-name)))
(log/warn (deferred-trs "Warning: endpoint {0}/{1} does not have a docstring." (ns-name *ns*) fn-name)))
`(def ~(vary-meta fn-name assoc
;; eval the vals in arg->schema to make sure the actual schemas are resolved so we can document
;; their API error messages
......@@ -266,7 +266,7 @@
[arg->schema body] (u/optional (every-pred map? #(every? symbol? (keys %))) more)
validate-param-calls (validate-params arg->schema)]
(when-not docstr
(log/warn (trs "Warning: endpoint {0}/{1} does not have a docstring." (ns-name *ns*) fn-name)))
(log/warn (deferred-trs "Warning: endpoint {0}/{1} does not have a docstring." (ns-name *ns*) fn-name)))
`(def ~(vary-meta fn-name assoc
;; eval the vals in arg->schema to make sure the actual schemas are resolved so we can document
;; their API error messages
......
......@@ -254,7 +254,7 @@
[response]
;; Not sure why this is but the JSON serialization middleware barfs if response is just a plain boolean
(when (m/boolean? response)
(throw (Exception. (str (tru "Attempted to return a boolean as an API response. This is not allowed!")))))
(throw (Exception. (tru "Attempted to return a boolean as an API response. This is not allowed!"))))
(if (and (map? response)
(contains? response :status)
(contains? response :body))
......
......@@ -31,7 +31,7 @@
[sync-metadata :as sync-metadata]]
[metabase.util
[cron :as cron-util]
[i18n :refer [tru]]
[i18n :refer [deferred-tru]]
[schema :as su]]
[schema.core :as s]
[toucan
......@@ -45,7 +45,7 @@
su/NonBlankString
#(u/ignore-exceptions (driver/the-driver %))
"Valid database engine")
(tru "value must be a valid database engine.")))
(deferred-tru "value must be a valid database engine.")))
;;; ----------------------------------------------- GET /api/database ------------------------------------------------
......
......@@ -65,7 +65,7 @@
(export-format->context :json) ;-> :json-download"
[export-format]
(or (get-in ex/export-formats [export-format :context])
(throw (Exception. (str (tru "Invalid export format: {0}" export-format))))))
(throw (Exception. (tru "Invalid export format: {0}" export-format)))))
(defn- datetime-str->date
"Dates are iso formatted, i.e. 2014-09-18T00:00:00.000-07:00. We can just drop the T and everything after it since
......
......@@ -6,7 +6,7 @@
[metabase.models.setting :as setting :refer [defsetting]]
[metabase.util :as u]
[metabase.util
[i18n :as ui18n :refer [tru]]
[i18n :as ui18n :refer [deferred-tru tru]]
[schema :as su]]
[ring.util.response :as rr]
[schema.core :as s])
......@@ -54,7 +54,7 @@
(valid-json? resource)
(catch JsonParseException e
(rethrow-with-message (tru "Unable to parse resource `{0}` as JSON" relative-path-with-prefix) e)))
(throw (FileNotFoundException. (str (tru "Unable to find JSON via relative path `{0}`" relative-path-with-prefix)))))))))
(throw (FileNotFoundException. (tru "Unable to find JSON via relative path `{0}`" relative-path-with-prefix))))))))
(defn- valid-json-url?
"Is URL a valid HTTP URL and does it point to valid JSON?"
......@@ -85,7 +85,7 @@
(memoize (fn [url-or-resource-path]
(or (valid-json-url? url-or-resource-path)
(valid-json-resource? url-or-resource-path)
(throw (Exception. (str (tru "Invalid JSON URL or resource: {0}" url-or-resource-path))))))))
(throw (Exception. (tru "Invalid JSON URL or resource: {0}" url-or-resource-path)))))))
(def ^:private CustomGeoJSON
{s/Keyword {:name s/Str
......@@ -112,7 +112,7 @@
(valid-json-url-or-resource? geo-url-or-uri)))
(defsetting custom-geojson
(tru "JSON containing information about custom GeoJSON files for use in map visualizations instead of the default US State or World GeoJSON.")
(deferred-tru "JSON containing information about custom GeoJSON files for use in map visualizations instead of the default US State or World GeoJSON.")
:type :json
:default {}
:getter (fn [] (merge (setting/get-json :custom-geojson) builtin-geojson))
......
......@@ -216,7 +216,7 @@
(matching-dashboard-param-with-target dashboard-params dashcard-param-mappings target)
;; ...but if we *still* couldn't find a match, throw an Exception, because we don't want people
;; trying to inject new params
(throw (Exception. (str (tru "Invalid param: {0}" slug)))))]]
(throw (Exception. (tru "Invalid param: {0}" slug))))]]
(merge query-param dashboard-param)))))
(defn- check-card-is-in-dashboard
......
......@@ -38,7 +38,7 @@
[metabase.middleware
[auth :as middleware.auth]
[exceptions :as middleware.exceptions]]
[metabase.util.i18n :refer [tru]]))
[metabase.util.i18n :refer [deferred-tru]]))
(def ^:private +generic-exceptions
"Wrap ROUTES so any Exception thrown is just returned as a generic 400, to prevent details from leaking in public
......@@ -91,4 +91,4 @@
(context "/transform" [] (+auth transform/routes))
(context "/user" [] (+auth user/routes))
(context "/util" [] util/routes)
(route/not-found (constantly {:status 404, :body (tru "API endpoint does not exist.")})))
(route/not-found (constantly {:status 404, :body (deferred-tru "API endpoint does not exist.")})))
......@@ -19,7 +19,7 @@
[setting :refer [defsetting]]
[user :as user :refer [User]]]
[metabase.util
[i18n :as ui18n :refer [trs tru]]
[i18n :as ui18n :refer [deferred-tru trs tru]]
[password :as pass]
[schema :as su]]
[schema.core :as s]
......@@ -47,8 +47,8 @@
;; IP Address doesn't have an actual UI field so just show error by username
:ip-address (throttle/make-throttler :username, :attempts-threshold 50)})
(def ^:private password-fail-message (tru "Password did not match stored password."))
(def ^:private password-fail-snippet (tru "did not match stored password"))
(def ^:private password-fail-message (deferred-tru "Password did not match stored password."))
(def ^:private password-fail-snippet (deferred-tru "did not match stored password"))
(s/defn ^:private ldap-login :- (s/maybe UUID)
"If LDAP is enabled and a matching user exists return a new Session for them, or `nil` if they couldn't be
......@@ -203,10 +203,10 @@
;; add more 3rd-party SSO options
(defsetting google-auth-client-id
(tru "Client ID for Google Auth SSO. If this is set, Google Auth is considered to be enabled."))
(deferred-tru "Client ID for Google Auth SSO. If this is set, Google Auth is considered to be enabled."))
(defsetting google-auth-auto-create-accounts-domain
(tru "When set, allow users to sign up on their own if their Google account email address is from this domain."))
(deferred-tru "When set, allow users to sign up on their own if their Google account email address is from this domain."))
(defn- google-auth-token-info [^String token]
(let [{:keys [status body]} (http/post (str "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" token))]
......
......@@ -75,7 +75,7 @@
;; setup database (if needed)
(when engine
(when-not (driver/available? engine)
(throw (ex-info (str (tru "Cannot create Database: cannot find driver {0}." engine))
(throw (ex-info (tru "Cannot create Database: cannot find driver {0}." engine)
{:engine engine})))
(let [db (db/insert! Database
(merge
......
......@@ -21,7 +21,7 @@
[table :as table :refer [Table]]]
[metabase.sync.field-values :as sync-field-values]
[metabase.util
[i18n :refer [trs tru]]
[i18n :refer [deferred-tru trs]]
[schema :as su]]
[schema.core :as s]
[toucan
......@@ -80,9 +80,9 @@
(sync/sync-table! updated-table))
updated-table)))
(def ^:private auto-bin-str (tru "Auto bin"))
(def ^:private dont-bin-str (tru "Don''t bin"))
(def ^:private day-str (tru "Day"))
(def ^:private auto-bin-str (deferred-tru "Auto bin"))
(def ^:private dont-bin-str (deferred-tru "Don''t bin"))
(def ^:private day-str (deferred-tru "Day"))
(def ^:private dimension-options
(let [default-entry [auto-bin-str ["default"]]]
......@@ -93,30 +93,30 @@
:mbql ["datetime-field" nil param]
:type "type/DateTime"})
;; note the order of these options corresponds to the order they will be shown to the user in the UI
[[(tru "Minute") "minute"]
[(tru "Hour") "hour"]
[[(deferred-tru "Minute") "minute"]
[(deferred-tru "Hour") "hour"]
[day-str "day"]
[(tru "Week") "week"]
[(tru "Month") "month"]
[(tru "Quarter") "quarter"]
[(tru "Year") "year"]
[(tru "Minute of Hour") "minute-of-hour"]
[(tru "Hour of Day") "hour-of-day"]
[(tru "Day of Week") "day-of-week"]
[(tru "Day of Month") "day-of-month"]
[(tru "Day of Year") "day-of-year"]
[(tru "Week of Year") "week-of-year"]
[(tru "Month of Year") "month-of-year"]
[(tru "Quarter of Year") "quarter-of-year"]])
[(deferred-tru "Week") "week"]
[(deferred-tru "Month") "month"]
[(deferred-tru "Quarter") "quarter"]
[(deferred-tru "Year") "year"]
[(deferred-tru "Minute of Hour") "minute-of-hour"]
[(deferred-tru "Hour of Day") "hour-of-day"]
[(deferred-tru "Day of Week") "day-of-week"]
[(deferred-tru "Day of Month") "day-of-month"]
[(deferred-tru "Day of Year") "day-of-year"]
[(deferred-tru "Week of Year") "week-of-year"]
[(deferred-tru "Month of Year") "month-of-year"]
[(deferred-tru "Quarter of Year") "quarter-of-year"]])
(conj
(mapv (fn [[name params]]
{:name name
:mbql (apply vector "binning-strategy" nil params)
:type "type/Number"})
[default-entry
[(tru "10 bins") ["num-bins" 10]]
[(tru "50 bins") ["num-bins" 50]]
[(tru "100 bins") ["num-bins" 100]]])
[(deferred-tru "10 bins") ["num-bins" 10]]
[(deferred-tru "50 bins") ["num-bins" 50]]
[(deferred-tru "100 bins") ["num-bins" 100]]])
{:name dont-bin-str
:mbql nil
:type "type/Number"})
......@@ -126,10 +126,10 @@
:mbql (apply vector "binning-strategy" nil params)
:type "type/Coordinate"})
[default-entry
[(tru "Bin every 0.1 degrees") ["bin-width" 0.1]]
[(tru "Bin every 1 degree") ["bin-width" 1.0]]
[(tru "Bin every 10 degrees") ["bin-width" 10.0]]
[(tru "Bin every 20 degrees") ["bin-width" 20.0]]])
[(deferred-tru "Bin every 0.1 degrees") ["bin-width" 0.1]]
[(deferred-tru "Bin every 1 degree") ["bin-width" 1.0]]
[(deferred-tru "Bin every 10 degrees") ["bin-width" 10.0]]
[(deferred-tru "Bin every 20 degrees") ["bin-width" 20.0]]])
{:name dont-bin-str
:mbql nil
:type "type/Coordinate"})))))
......
......@@ -103,7 +103,7 @@
(let [output-stream (ByteArrayOutputStream.)]
(try
(when-not (ImageIO/write tile "png" output-stream) ; returns `true` if successful -- see JavaDoc
(throw (Exception. (str (tru "No appropriate image writer found!")))))
(throw (Exception. (tru "No appropriate image writer found!"))))
(.flush output-stream)
(.toByteArray output-stream)
(catch Throwable e
......@@ -153,7 +153,7 @@
;; make sure query completed successfully, or API endpoint should return 400
_
(when-not (= status :completed)
(throw (ex-info (str (tru "Query failed"))
(throw (ex-info (tru "Query failed")
;; `result` might be a `core.async` channel or something we're not expecting
(assoc (when (map? result) result) :status-code 400))))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment