GithubHelp home page GithubHelp logo

schematic's Introduction

walmartlabs/schematic

Clojars Project

CircleCI

Schematic is a Clojure library which aids in assembling Component systems from configuration data. That configuration data is often read from an edn file. The structure of the configuration data was inspired by Integrant. The goal of Schematic is to reduce the complexity of programmatic construction of Components, while continuing to use existing Component workflows and tools.

The primary features provided by Schematic are:

  • specifying an initialization function for a Component
  • declaring Component dependencies (for use in component/using)
  • declaring merge definitions to enable configuration sharing among Components
  • assembling a System using only a subset of the available Components

API Documentation

Blog Post

Overview

Schematic starts with a configuration map. From this map, Schematic will assemble a Component system map, ready to be started.

Each key/value pair in the configuration map will become a system key and component in the Component system map.

Schematic uses some special keys to identify

  • The constructor for the component (:sc/create-fn)
  • The Component dependencies for the components (:sc/refs)
  • Additional rules to manipulate the component's configuration (:sc/merge)

The component's configuration is passed to its constructor as part of building the Component system map.

It is ok to omit the :sc/create-fn constructor when a plain map is sufficient. This may occur when the component doesn't need to implement any protocols, or when the component exists as a source of configuration for other components.

Usage

The simplest example is:

(require '[com.walmartlabs.schematic :as sc]
         '[com.stuartsierra.component :as component])
 
(def config {:app {:host "localhost"}})  
       
(->> (sc/assemble-system config)

     (component/start-system)
     (into {}) ;; put it into a map just so it prints in the REPL
     )
     
;; => {:app {:host "localhost"}}

In this case, Schematic didn't really do any work, since there were no declared refs, merge-defs, or create-fns.

A more complete example is:

(require '[com.walmartlabs.schematic :as sc]
         '[com.stuartsierra.component :as component])

(defrecord App [server api-keys]
  component/Lifecycle
  (start [this]
   (assoc this :started true))
  
  (stop [this]
   (assoc this :started nil)))

(def config
  {:app {:sc/create-fn `map->App
         :sc/refs {:server :webserver}
         :sc/merge [{:to [:api-keys] :from [:api :keys]}]}
  
   :webserver {:sc/merge [{:from [:host-config]}]
               :port 8080}
   
   :host-config {:host "localhost"}
  
   :api {:username "user"
         :keys [1 2 3 4]}})
   
(->> (sc/assemble-system config)
     (component/start-system)
     (into {}) ;; put it into a map just so it prints in the REPL
     )

;; =>
;; {:api {:username "user", :keys [1 2 3 4]},
;;  :host-config {:host 8080},
;;  :webserver {:host "localhost", :port 8080},
;;  :app #user.App{:server {:host "localhost", :port 8080}, 
;;                 :api-keys [1 2 3 4], 
;;                 :started true}}

Notice, in the above, that the Schematic keys (:sc/create-fn, etc.) have been removed, and the configuration for the :app component has been extended via :sc/merge.

Of course, in a more realistic example, the configuration data would be read from an EDN file, rather than hard-coded into the application.

The following sections will cover aspects seen in the above example.

Declaring configuration components

Example (edn):

{:app {:sc/create-fn user/map->App
       :sc/refs {:server :webserver
                 :api-keys :api-keys}}

 :webserver {:sc/refs [:host]
             :port 8080}
             
 :host 8080

 :api-keys [1 2 3 4]}

In this example, :app, :webserver, host and :api-keys are all top-level items, and will be used to create components and/or be injected into components. Top-level items can be any type of data, but only associative data structures will receive dependency injection metadata for their references.

Declaring dependencies

Component dependencies are declared via a key of :sc/refs with the value being a map or vector having the same semantics as component/using.

Example:

:app {:sc/create-fn user/map->App
      :sc/refs {:server :webserver
                :api-keys :api-keys}}
                
:webserver {:sc/refs [:host]}

In :app, the refs are a map of component-ids, where the key is the local id of the component, and the value is the global id of the component. In :webserver, a vector is provided, indicating that the local id and global id of the component are the same.

Creating Components

To specify that a particular configuration item can participate in component/Lifecycle functions (start/stop), the special key -- :sc/create-fn -- is used to declare the namespace-qualified name of the function which which will be called to create the Component instance. This will usually be the default map->Record function which defrecord creates for us. But it can also be any regular function which returns an object which implements the Lifecycle interface, and accepts a single map as an argument.

Example (using a map->Record constructor function):

(ns com.business.system
  (:require [com.walmartlabs.schematic :as sc]
            [com.stuartsierra.component :as component]))

(defrecord App [server api-keys thread-pool-size]
  component/Lifecycle
  (start [this] this)
  (stop [this] this))
  
(def config {:app {:sc/create-fn 'com.business.system/map->App
                   :sc/refs {:server :webserver
                             :api-keys :api-keys}
                   :thread-pool-size 20}
             ;; remaining config omitted     
             })

Note :sc/create-fn 'com.business.system/map->App which will cause the App record to be created when the system is assembled.

Example (using plain function):

(ns com.business.system
  (:require [com.walmartlabs.schematic :as sc]
            [com.stuartsierra.component :as component]))

(defrecord App [server api-keys thread-pool-size]
  component/Lifecycle
  (start [this] this)
  (stop [this] this))
  
(defn make-app [{:keys [thread-pool-size] :or {thread-pool-size 10}}]
  (map->App {:thread-pool-size thread-pool-size}))
  
(def config {:app {:sc/create-fn 'com.business.system/make-app
                   :sc/refs {:server :webserver
                             :api-keys :api-keys}
                   :thread-pool-size 20}
             ;; remaining config omitted     
             })

In the 'plain function' example, you can see that it is possible to set a default value for thread-pool-size in the make-app function if a value were not to be provided in the config. That is just one possible use case for using a plain function constructor.

Dependency injection

For each top-level, map? item in the config, Schematic will extract a list of the declared :sc/refs and attach the needed Component metadata by calling (component/using component ref-map). The resulting value will be a Component which can be started and stopped.

Merging common configuration

In complex applications, there may be configuration that is shared or duplicated across components, the :sc/merge key is a set of rules for selecting, renaming, and injecting global configuration into a component's specific configuration.

The merge happens before :sc/refs are processed.

:host-config {:host "localhost"
              :port 8080}

:app {:sc/merge [{:from [:host-config]}]}

In this example, :host-config map will be merged into the :app map.

  • :sc/merge accepts a vector of merge-defs.
  • a merge-def can provide :to, :from, and :select keys
  • :to indicates the key/key-path in the component config where the results will be merged. It defaults to [].
  • :from indicates the key/key-path from the global config where values will be copied from
  • :select is either a vector of key-names or a map of local-key-names to from-key-names (names of fields in :from). It defaults to the special keyword :all, which indicates to copy the entire :from data object.

Assembling the system

The actual work of processing merge-defs, analyzing the :sc/refs, and adding the dependency metadata happens in the com.walmartlabs.schematic/assemble-system function. That function takes a config map and returns a System map which can be started and stopped.

Example:

(->> {:webserver {:sc/refs [:host :port]}
      :host "localhost"
      :port 8080}
     (sc/assemble-system)
     (component/start-system))

;; => #<SystemMap>

assemble-system optionally takes a configuration map and an optional list of component-ids to include in the final system.

Assembling a sub-system using component-ids

In some cases it might be desirable to include only a subset of the available config when assembling a system, so that only the needed components are started and stopped. Two possible such scenarios are:

  • The config is shared among multiple applications, which each need a portion of the components at runtime, but not all of them.
  • At the REPL, it might be useful to get a particular component and start it, such as a database connection.

In such cases, the two argument version of assemble-system can be invoked, passing the list of component ids as the second parameter. There are the top-level components which must be included in the final system. Only these top-level components and all of their transitive dependencies will be included in the final system map.

Example (include single app):

(-> {:app-1 {:sc/refs {:conn :db-conn
                       :product-api :product-api}}
     :app-2 {:sc/refs {:conn :db-conn
                       :customer-api :customer-api}}
     :db-conn {:sc/refs {:host :db-host}
               :username "user"
               :password "secret"}
     :product-api {}
     :customer-api {}
     :db-host "localhost"}
    (sc/assemble-system [:app-1])
    (component/start-system)
    ((partial into {})))
;; =>
;; {:product-api {},
;;  :app-1 {:conn {:host "localhost", :username "user", :password "secret"}, :product-api {}},
;;  :db-host "localhost",
;;  :db-conn {:host "localhost", :username "user", :password "secret"}}

Notice that :app-2 and :customer-api have not been included in the final system.

This filtering of components occurs before components are properly instantiated (via the :sc/create-fn function).

Example (development at the REPL):

(let [config {:app-1 {:sc/refs {:conn :db-conn
                                :product-api :product-api}}
              :app-2 {:sc/refs {:conn :db-conn
                                :customer-api :customer-api}}
              :db-conn {:sc/refs {:host :db-host}
                        :username "user"
                        :password "secret"}
              :product-api {}
              :customer-api {}
              :db-host "localhost"}
      system (-> (sc/assemble-system config [:db-conn :product-api])
                 (component/start-system))
      {:keys [db-conn product-api]} system]
  (println (into {} system))
  ;; do something here with db-conn or product-api
  (component/stop system)
  nil)

;; {:product-api {}, 
;;  :db-host localhost, 
;;  :db-conn {:host localhost, :username user, :password secret}}    
;; => nil 

In this example, we were able to use just the database component and product-api to do some testing in the REPL.

License

Copyright (c) 2017-present, Walmart Inc.

Distributed under the Apache Software License 2.0.

schematic's People

Contributors

bcarrell avatar candid82 avatar hlship avatar sashton 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

schematic's Issues

"MIssing definitions for refs" exception fails to identify the container of the bad refs

         com.walmartlabs.schematic/assemble-system   schematic.clj:  305
        com.walmartlabs.schematic/validate-config!   schematic.clj:  260
   com.walmartlabs.schematic/throw-on-missing-refs   schematic.clj:   86
                              clojure.core/ex-info        core.clj: 4739
             clojure.lang.ExceptionInfo: Missing definitions for refs: :manager, :executor-service
    missing-refs: (:manager :executor-service)
          reason: :com.walmartlabs.schematic/missing-refs
clojure.lang.Compiler$CompilerException: clojure.lang.ExceptionInfo: Missing definitions for refs: :manager, :executor-service {:reason :com.walmartlabs.schematic/missing-refs, :missing-refs (:manager :executor-service)}, compiling:(/Users/hlship/workspaces/walmart/clockwork/dev-resources/demo.clj:84:3)

This exception should identify the component, as I'm having trouble figuring out what component is actually broken.

New Clojars release?

I've updated this issue to better reflect my question, didn't look carefully enough how 1.2.0 and master differ ๐Ÿ˜… .

There is an issue with subconfig-for-components in 1.2.0 which is fixed in master. The old implementation doesn't track which components are seen and it's possible that the list of ref-ids in the loop keeps getting bigger and bigger resulting in a StackOverflowError.

So perhaps it's nice to push a new version of Schematic to Clojars? thanks!


Original:

We've bumped into an issue today with schematic and where surpriced to see that the code published to clojuars under version 1.2.0 seems to be an older version.

You can check this by checking the .m2 directory and checking the contents of schematic.clj which includes:

(defn ^:no-doc subconfig-for-components
  "Returns a map of just the given components and their transitive dependencies"
  [system-config component-ids]
  (let [dep-graph (ref-dependency-graph system-config)]
    (loop [ref-ids component-ids deps (set component-ids)]
      (if-let [ref-id (first ref-ids)]
        (let [new-deps (get-in dep-graph [:dependencies ref-id])]
          (recur (concat (rest ref-ids) new-deps) (into deps new-deps)))
        (select-keys system-config deps)))))

This seems to be the implementation of version 1.1.0 whereas 1.2.0 looks like this. The project.clj in the .m2 dir does seem to mention 1.2.0.

We've fixed our issue by depending on a github sha via tools.deps copying the code to our project, but I hope you're able to fix the Clojars release ๐Ÿ˜ƒ .

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.