Giter VIP home page Giter VIP logo

spectomic's Introduction

spectomic

CircleCI

Generate Datomic or Datascript schema from your Clojure(script) specs.

Installation

[provisdom/spectomic "1.0.78"] ;; latest release

Usage

Let's take a look at a basic example.

(require '[clojure.spec :as s]
         '[provisdom.spectomic.core :as spectomic])
=> nil

(s/def ::string string?)
=> :boot.user/string

(spectomic/datomic-schema [::string])
=> [{:db/ident       :boot.user/string
     :db/valueType   :db.type/string
     :db/cardinality :db.cardinality/one}]

In this example we define the spec ::string and then pass it to datomic-schema which returns a collection of schema matching the data our ::string spec represents. Now let's look at a more complicated example.

(s/def :entity/id uuid?)
=> :entity/id

(s/def :user/name string?)
=> :user/name

(s/def :user/favorite-foods (s/coll-of string?))
=> :user/favorite-foods

(s/def :order/name string?)
=> :order/name

(s/def :user/order (s/keys :req [:entity/id :order/name]))
=> :user/orders

(s/def :user/orders (s/coll-of :user/order))
=> :user/orders

(s/def ::user (s/keys :req [:entity/id :user/name :user/favorite-foods :user/orders]))
=> :boot.user/user

(spectomic/datomic-schema [[:entity/id {:db/unique :db.unique/identity
                                        :db/index  true}]
                           :user/name
                           :user/favorite-foods
                           :user/orders])
=> [{:db/ident       :entity/id
     :db/valueType   :db.type/uuid
     :db/cardinality :db.cardinality/one
     :db/unique      :db.unique/identity
     :db/index       true}
    {:db/ident       :user/name
     :db/valueType   :db.type/string
     :db/cardinality :db.cardinality/one}
    {:db/ident       :user/favorite-foods
     :db/valueType   :db.type/string
     :db/cardinality :db.cardinality/many}
    {:db/ident       :user/orders
     :db/valueType   :db.type/ref
     :db/cardinality :db.cardinality/many}]

In this example we have a ::user entity that is uniquely identified by an :entity/id. The user also has a collection of his or her favorite foods :user/favorite-foods and a collection of orders :user/orders.

We need to let datomic-schema know that :entity/id is unique. We do this be providing datomic-schema with a tuple instead of just the spec keyword. The first element of the tuple is the spec and the second element is any extra schema fields to be added to the attribute. In this case we attach :db/unique and :db/index. You can see in the outputted schema that :entity/id does indeed have those extra fields.

:user/name is not particularly interesting as it is similar to our basic example.

:user/favorite-foods is represented as a collection of strings in our example. And as you can see in the returned schema, :user/favorite-foods is :db.cardinality/many and :db.type/string.

:user/orders is a collection of maps of the for dictated by the spec ::orders. And our returned schema is of type :db.type/ref and :db.cardinality/many.

Resolving Custom Types

Your code may use types that are not resolvable to Datomic types with the default type type resolver implementation. One option to this problem is to use the schema entry format you saw above with :entity/id. If you recall, we set the :db/unique property for :entity/id to :db.unique/identity. You can actually manually set the :db/valueType too. This could, however, become very repetitive.

The second option is to pass a map with the :custom-type-resolver key set to a function that returns a Datomic type. If the default type resolver cannot resolve an object's type, your function will be called with the object passed to it as the only argument. Your function is expected to return a valid Datomic type, as defined here.

Usage in Production

This library provides two functions for generating schema from specs: datomic-schema and datascript-schema. Both function calls occur at runtime. This means that if you include this library as a runtime dependency, test.check will also be included. Often you don't want test.check as a runtime dependency so it is suggested that you include this dependency as test scope (e.g. [provisdom/spectomic "x.x.x" :scope "test"]). Including this library with test scope means that all your calls need to happen at compile time or elsewhere. Here are two examples of how this library can be used at compile time.

Macro

It is convenient to store the list of specs you want to convert into schema as a var. By doing so you are able to easily add new specs to the schema list at any time. We will use a def'ed var as an example.

(def schema-specs [[:entity/id {:db/unique :db.unique/identity :db/index true}] :user/name :user/favorite-foods])

Now let's write a def-like macro that will generate our schema at compile time:

(defmacro defschema
  [name]
  `(def ~name (spectomic/datomic-schema schema-specs)))

(defschema my-schema)

Now our schema is statically compiled and available in the var my-schema.

Build time

Sometimes you may want to save your schema into an actual EDN file for use in multiple places. This can be easily accomplished with a Boot task.

(require '[provisdom.spectomic.core :as spectomic])

(deftask generate-schema
  [s sym VAL sym "This symbol will be resolved and have its content passed to `datomic-schema`."]
  (spit "my-schema.edn" (spectomic/datomic-schema @(resolve sym))))

This task is simple but does not adhere to Boot's design patterns. Here's a slightly more complex example that integrates well with other tasks.

(require '[clojure.java.io :as io])

(deftask generate-schema
  [s sym VAL sym "This symbol will be resolved and have its content passed to `datomic-schema`."
   o out-file VAL str "Name of the file for the schema to be outputted to. Defaults to schema.edn"]
  (let [specs-var (resolve sym)]
    (assert specs-var "The symbol provided cannot be resolved.")
    (with-pre-wrap fileset
      (let [out-file-name (or out-file "schema.edn")
            out-dir (tmp-dir!)
            out-file (io/file out-dir out-file-name)]
        (spit out-file (spectomic/datomic-schema @specs-var))
        (commit! (add-resource fileset out-dir))))))

Caveats

Misleading Generators

When writing your specs you need to be mindful of the generator that is used for the spec. One misleading predicate generator is float?. float?'s generator actually uses the same generator as double?, meaning it does not return a number with type java.lang.Float. This is problematic for our schema generator as it relies on your objects having the correct type that they represent. This is not a bug in Clojure spec however. If you look at how float? is defined you will see that float? returns true if the object is a java.lang.Double or a java.lang.Float. To combat this we can write our own ::float spec like this:

(s/def ::float
  (s/with-gen
    #(instance? java.lang.Float %)
    #(gen/fmap float
               (s/gen (s/double-in :min Float/MIN_VALUE :max Float/MAX_VALUE :infinite? false :NaN? false)))))

Rare Edge Cases

If you write a Spec that uses a generator that will return values of a different type very rarely then you will run into schema generation problems. Make sure your generators are returning consistent types.

Implementation

Rather than parsing every Spec form, we chose to implement this library using generators. Every Spec that is passed to datomic-schema or datascript-schema is sampled 100 times using test.check. The type of each sample is then matched with a Datomic type. Next we make sure that each sample is of the same type. If your generator is returning multiple types for a Spec then it's not clear how the schema should be generated so an error is thrown. If the type is a collection then we need to verify that for each sample, every element in the collection is of the same type. If that is true then we return a map with :db.cardinality/many and the :db/valueType set to the type of object the collection contains.

Because generating samples for specs can end up taking a significant amount of time, we do some Spec form parsing up front to try and determine the Datomic type. If the Spec form is not handled then we fall back on generation.

License

Copyright © 2017 Provisdom

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

spectomic's People

Contributors

c0rrzin avatar kennyjwilli avatar punit-naik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spectomic's Issues

Add syntax checking

When including schema attributes in the form [::spec {db/...}], should check that the vector has only two elements, first is a spec, second is a map, etc.

Inferring ref attributes

A couple of potential issues exist when inferring ref attributes simply via the predicate map?. One is that it implies some semantics about how you're handling the map if specified in transaction data, because map? does not require the existence of a :db/id key or an attribute indicating identity. One might assume, for example, if :db/id is absent then a new entity will be created. But that will be application-specific.

The other issue is that many use-cases call for an actual entity ID as opposed to a map. And though you could infer a ref in some cases (tempid is an instance of datomic.db.DbId, lookup ref is a tuple of keyword? and any?), I don't know how you disambiguate between longs and entity IDs, or keywords and :db/ident.

The schema override feature basically solves this, let's you explicitly denote the attribute as a ref. If we want the feature to infer ref attributes, maybe the user could supply a predicate reflecting their application semantics (e.g. if map? was appropriate for their app, then they could specify map? as the predicate). It might be nice, just because it makes those semantics explicit in the spec. You'd still have the potential ambiguous cases, but those probably just have to be handled by the override.

Missing statement in README.md

The code block:

(spectomic/datomic-schema [[:entity/id {:db/unique :db.unique/identity :db/index true}] :user/name :user/favorite-foods])

Does not have the statement user/orders in it. So it should become:

(spectomic/datomic-schema [[:entity/id {:db/unique :db.unique/identity :db/index true}] :user/name :user/favorite-foods :user/orders])

Possible typo?

I was going through the readme and I found something confusing. It might be a typo or something I
am not quite getting. It would make sense to me if the first outlined item in my image below is more like (s/def ::order (s/keys :req [:entity/id :order/name]))

I have not run this myself to verify. When I have time later I'll try.

spectomic generate datomic or datascript schema from your clojure script specs 2017-07-22 22-27-18

Can't generate schema of a composite spec

Currently, if a spec if defined using s/merge or s/keys i.e. a composite spec, generating datomic schema out of it using spectomic.core/datomic-schema does not generate schema for all the individual specs it is made of.
If this is implemented, it will alleviate the pain of individually populating the spec vector to be fed to the spectomic.core/datomic-schema function.
Overriding of the individual specs' schemas should still be possible.

Allow :db/valueType to be explicitly specified

Sometimes it will be useful to explicitly specify :db/valueType, e.g. if for some reason a predicate can't generate appropriate values. If :db/type is specified, just take it as-is and don't perform type inference.

Adding extra predicates 100 tries error.

I'm not really sure if it's worth the trouble to do something about this, but I thought I'd report it since it is somewhat normal usage. I've worked around it by writing the spec less generically and using more of them, but I've got a spec described as;

(s/def :dp.attr/dt-kw (s/and :dp.attr/keyword #(kw-in-ns-hierarchy? % "dp.dt")))

Where the predicate function is,

(defn kw-in-ns-hierarchy? "Takes a keyword and a partial or complete namespace and responds true if the keyword is in the hierarchy." [kw, ns-partial-string] (clojure.string/starts-with? (namespace kw) ns-partial-string))

Meaning the keyword could be anything that starts with :dp.dt. This blows up the generation stage.

=> Execution error (ExceptionInfo) at provisdom.spectomic.core/find-type-via-generation$fn (core.clj:79). Couldn't satisfy such-that predicate after 100 tries.

This may be a hard limitation, no biggie over here. Just thought you should know. I'm enjoying having the library in my workflow. Thanks for sharing!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.