¿Cómo puedo usar mis especificaciones para los fines previstos si están en un espacio de nombres separado?


Uno de los ejemplos en el clojure.spec Guide es una opción simple-especificación de análisis:

(require '[clojure.spec :as s])

(s/def ::config
  (s/* (s/cat :prop string?
              :val (s/alt :s string? :b boolean?))))

(s/conform ::config ["-server" "foo" "-verbose" true "-user" "joe"])
;;=> [{:prop "-server", :val [:s "foo"]}
;;    {:prop "-verbose", :val [:b true]}
;;    {:prop "-user", :val [:s "joe"]}]

Más tarde, en la sección validación , se define una función que internamente conforms su entrada usando esta especificación:

(defn- set-config [prop val]
  (println "set" prop val))

(defn configure [input]
  (let [parsed (s/conform ::config input)]
    (if (= parsed ::s/invalid)
      (throw (ex-info "Invalid input" (s/explain-data ::config input)))
      (doseq [{prop :prop [_ val] :val} parsed]
        (set-config (subs prop 1) val)))))

(configure ["-server" "foo" "-verbose" true "-user" "joe"])
;; set server foo
;; set verbose true
;; set user joe
;;=> nil

Dado que la guía está destinada a ser fácil de seguir desde el REPL, todo este código se evalúa en el mismo espacio de nombres. En esta respuesta, sin embargo, @levand recomienda poner las especificaciones en espacios de nombres separados:

I por lo general, ponga las especificaciones en su propio espacio de nombres, junto con el espacio de nombres que están describiendo.

Esto rompería el uso de ::config anterior, pero ese problema puede ser remediado:

Es preferible que los nombres de clave spec estén en el espacio de nombres del código, sin embargo, no en el espacio de nombres de la especificación. Esto sigue siendo fácil de hacer mediante el uso de un alias de espacio de nombres en la palabra clave:

(ns my.app.foo.specs
  (:require [my.app.foo :as f]))

(s/def ::f/name string?)

Continúa explicando que las especificaciones y las implementaciones podrían en el mismo espacio de nombres, pero no sería ideal:

Aunque ciertamente podría ponerlos junto al código spec'd en el mismo archivo, eso perjudica la legibilidad IMO.

Sin embargo, estoy teniendo problemas para ver cómo esto puede funcionar con desestructuración. Como ejemplo, armé un pequeño proyecto Boot con el código anterior traducido a múltiples espacios de nombres.

boot.properties:

BOOT_CLOJURE_VERSION=1.9.0-alpha7

src/example/core.clj:

(ns example.core
  (:require [clojure.spec :as s]))

(defn- set-config [prop val]
  (println "set" prop val))

(defn configure [input]
  (let [parsed (s/conform ::config input)]
    (if (= parsed ::s/invalid)
      (throw (ex-info "Invalid input" (s/explain-data ::config input)))
      (doseq [{prop :prop [_ val] :val} parsed]
        (set-config (subs prop 1) val)))))

src/example/spec.clj:

(ns example.spec
  (:require [clojure.spec :as s]
            [example.core :as core]))

(s/def ::core/config
  (s/* (s/cat :prop string?
              :val (s/alt :s string? :b boolean?))))

build.boot:

(set-env! :source-paths #{"src"})

(require '[example.core :as core])

(deftask run []
  (with-pass-thru _
    (core/configure ["-server" "foo" "-verbose" true "-user" "joe"])))

Pero por supuesto, cuando realmente corro esto, obtengo un error:{[54]]}

$ boot run
clojure.lang.ExceptionInfo: Unable to resolve spec: :example.core/config

Podría solucionar este problema agregando (require 'example.spec) a build.boot, pero eso es feo y propenso a errores, y solo lo será más a medida que aumente mi número de espacios de nombres de especificaciones. No puedo require el espacio de nombres spec desde el espacio de nombres de implementación, por varias razones. Aquí hay un ejemplo que usa fdef.

boot.properties:

BOOT_CLOJURE_VERSION=1.9.0-alpha7

src/example/spec.clj:

(ns example.spec
  (:require [clojure.spec :as s]))

(alias 'core 'example.core)

(s/fdef core/divisible?
  :args (s/cat :x integer? :y (s/and integer? (complement zero?)))
  :ret boolean?)

(s/fdef core/prime?
  :args (s/cat :x integer?)
  :ret boolean?)

(s/fdef core/factor
  :args (s/cat :x (s/and integer? pos?))
  :ret (s/map-of (s/and integer? core/prime?) (s/and integer? pos?))
  :fn #(== (-> % :args :x) (apply * (for [[a b] (:ret %)] (Math/pow a b)))))

src/example/core.clj:

(ns example.core
  (:require [example.spec]))

(defn divisible? [x y]
  (zero? (rem x y)))

(defn prime? [x]
  (and (< 1 x)
       (not-any? (partial divisible? x)
                 (range 2 (inc (Math/floor (Math/sqrt x)))))))

(defn factor [x]
  (loop [x x y 2 factors {}]
    (let [add #(update factors % (fnil inc 0))]
      (cond
        (< x 2) factors
        (< x (* y y)) (add x)
        (divisible? x y) (recur (/ x y) y (add y))
        :else (recur x (inc y) factors)))))

build.boot:

(set-env!
 :source-paths #{"src"}
 :dependencies '[[org.clojure/test.check "0.9.0" :scope "test"]])

(require '[clojure.spec.test :as stest]
         '[example.core :as core])

(deftask run []
  (with-pass-thru _
    (prn (stest/run-all-tests))))

El primer problema es El más obvio:

$ boot run
clojure.lang.ExceptionInfo: No such var: core/prime?
    data: {:file "example/spec.clj", :line 16}
java.lang.RuntimeException: No such var: core/prime?

En mi especificación para factor, quiero usar mi predicado prime? para validar los factores devueltos. Lo bueno de esta especificación factor es que, suponiendo que prime? es correcta, documenta completamente la función factor y elimina la necesidad de escribir otras pruebas para esa función. Pero si crees que es demasiado genial, puedes reemplazarlo con pos? o algo así.

Sin embargo, como era de esperar, todavía obtendrá un error cuando intente boot run de nuevo, esta vez quejándose de que falta la especificación :args para #'example.core/divisible? o #'example.core/prime? o #'example.core/factor (lo que suceda al intentarlo primero). Esto es porque, independientemente de si usted alias un espacio de nombres o no, fdef no usará ese alias a menos que el símbolo que le dé nombre a un var que ya existe. Si el var no existe, el símbolo no se expande. (Para aún más diversión, quite el :as core de build.boot y vea lo que sucede.)

Si desea mantener ese alias, debe eliminar el (:require [example.spec]) de example.core y agregar un (require 'example.spec) a build.boot. Por supuesto, eso requiretiene que venir después de el de example.core, o no funcionará. Y en ese punto, ¿por qué no poner el require directamente en example.spec?

Todos estos problemas se resolverían poniendo las especificaciones en el mismo archivo que las implementaciones. Por lo tanto, ¿debería realmente poner las especificaciones en espacios de nombres separados de las implementaciones? Si es así, ¿cómo se pueden resolver los problemas que he detallado anteriormente?

Author: Community, 2016-06-25

1 answers

Esta pregunta demuestra una distinción importante entre las especificaciones utilizadas dentro de una aplicación y las especificaciones utilizadas para probar la aplicación.

Las especificaciones utilizadas dentro de la aplicación para conformar o validar la entrada, como :example.core/config aquí, son parte del código de la aplicación. Pueden estar en el mismo archivo donde se utilizan o en un archivo separado. En este último caso, el código de la aplicación debe :require las especificaciones, al igual que cualquier otro código.

Las especificaciones utilizadas como pruebas se cargan después de el código que especifican. Estos son sus fdefs y generadores. Puede ponerlos en un espacio de nombres separado del código, incluso en un directorio separado, no empaquetado con su aplicación, y :require el código.

Es posible que tenga algunos predicados o funciones de utilidad que son utilizadas por ambos tipos de especificaciones. Estos irían en un espacio de nombres separado, todos ellos propios.

 6
Author: Stuart Sierra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-09 19:56:08