{"id":29892959,"url":"https://github.com/siilisolutions/hedge","last_synced_at":"2025-08-01T02:18:04.438Z","repository":{"id":57356756,"uuid":"68811455","full_name":"siilisolutions/hedge","owner":"siilisolutions","description":"a serverless solution for clojure","archived":false,"fork":false,"pushed_at":"2018-09-21T10:57:07.000Z","size":258,"stargazers_count":39,"open_issues_count":36,"forks_count":6,"subscribers_count":13,"default_branch":"develop","last_synced_at":"2025-06-23T16:16:19.467Z","etag":null,"topics":["azure-functions","clojure","clojurescript","serverless-framework"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/siilisolutions.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-09-21T11:45:45.000Z","updated_at":"2024-05-31T07:55:58.000Z","dependencies_parsed_at":"2022-09-26T16:31:59.405Z","dependency_job_id":null,"html_url":"https://github.com/siilisolutions/hedge","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/siilisolutions/hedge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siilisolutions%2Fhedge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siilisolutions%2Fhedge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siilisolutions%2Fhedge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siilisolutions%2Fhedge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/siilisolutions","download_url":"https://codeload.github.com/siilisolutions/hedge/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siilisolutions%2Fhedge/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266858574,"owners_count":23996075,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-24T02:00:09.469Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["azure-functions","clojure","clojurescript","serverless-framework"],"created_at":"2025-08-01T02:18:03.178Z","updated_at":"2025-08-01T02:18:04.427Z","avatar_url":"https://github.com/siilisolutions.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hedge · [![Join the chat at https://gitter.im/siilisolutions/hedge](https://badges.gitter.im/siilisolutions/hedge.svg)](https://gitter.im/siilisolutions/hedge?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge) [![License](https://img.shields.io/badge/License-EPL%201.0-red.svg)](https://opensource.org/licenses/EPL-1.0) [![Build Status](https://travis-ci.org/siilisolutions/hedge.svg?branch=master)](https://travis-ci.org/siilisolutions/hedge) [![cljdoc badge](https://cljdoc.xyz/badge/siili/hedge)](https://cljdoc.xyz/d/siili/hedge/CURRENT)\n\n![Hedge](docs/images/hedgecube.png \"Hedge\")\n\nHedge is a platform agnostic ClojureScript framework for deploying ring compatible handlers to various environments with focus on serverless deployments.\n\n## Repository Contents\n\n - [/library](/library) contains wrapping and transformation code for adapting handlers to various platforms\n - [/boot](/boot) contains Hedge specific Boot tasks for code generation\n - [/acceptance](/acceptance) contains Concordion based acceptance test suite which also works as technical documentation on how to use Hedge\n\n## Known Limitations\n\n - asynchronous ring handlers not yet supported\n\n## Supported Platforms\n\n### [Azure Functions](https://azure.microsoft.com/en-us/services/functions/)\n### [AWS Lambda](https://aws.amazon.com/lambda/)\n\n## License\n\nCopyright © 2016-2018 [Siili Solutions Plc.](http://www.siili.com)\n\nDistributed under the [Eclipse Public License 1.0.](https://www.eclipse.org/legal/epl-v10.html)\n\n## Table of Contents\n1. Forewords\n1. Preparations and Required Software\n1. How Hedge Works\n1. Preparing Hedge Authentication Against Azure\n1. Authentication for AWS\n1. Supported Handler Types\n1. Input/Output Bindings\n1. Logging\n1. Basic Serverless Function Project Structure\n1. hedge.edn\n1. Handler signatures\n1. Exception Handling inside Serverless Function\n1. Testing\n1. Deploying to Azure\n1. Deploying to AWS   \n1. Other Usage Examples\n\n### Forewords\n\nThis document gets you started writing and deploying serverless functions to different environments. Hedge is under development, so things can change.\n\n### Preparations and Required Software\n\n* Java JDK 8 - Boot runs on JVM\n* [Boot-CLJ](https://github.com/boot-clj/boot) - Required to build ClojureScript projects and use Hedge\n* [Node.js](https://nodejs.org/en/download/) - Required for running unit tests\n* [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) - (Optional) Can be used when creating Service Principal for Azure and managing Azure resources\n* [AWS CLI](https://aws.amazon.com/cli/) - (Optional) Can be used to create credentials and to remove old test stacks from AWS\n\n### How Hedge Works\n\nClojureScript is compiled and optimized into JavaScript than can be run on Node.js or similar runtime environment available in serverless environments. Hedge takes care of generating code that is compatible and runnable on the deployment target. Hedge can then use provided authentication profile and deploy the compiled code to the serverless environment.\n\n### Preparing Hedge Authentication Against Azure\n\nRequirements: You must be subscription owner to be able to create service principals, or ask a subscription owner to generate the principal for you. This document describes how you can create service principal with Azure CLI from the command line.\n\nIf you haven't already performed log in with Azure CLI, do it and follow on-screen instructions.\n\n    az login\n\nMake sure you are using correct subscription\n\n    az account list --output table\n\nOptionally change the subscription\n\n    az account set --subscription \"\u003cyour subscription name\u003e\"\n\nExample: Create Service Principal with the name \"MyNameServicePrincipal\"\n\n    az ad sp create-for-rbac --name \"MyNameServicePrincipal\" --sdk-auth true \u003e MyNameServicePrincipal.json\n\nHint: Use a meaningful name that you can identify, so you can find it later if you need to remove it. Keep the generated file in a secure place, because it contains contributor role credentials by default that are able to affect things in your whole subscription.\n\nConfigure your environment\n\n    export AZURE_AUTH_LOCATION=/path-to-your/MyNameServicePrincipal.json\n\n### Authentication Against AWS\n\nEasiest way to create credentials for AWS to follow [AWS guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-quick-configuration) for their CLI.\n\n1. Create new access key and secret access key in [AWS IAM](https://console.aws.amazon.com/iam/home?#home) console\n2. Setup credentials locally\n  * By running command `aws configure` or\n  * By creating `~/.aws/credentials` file [manually](https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html)\n\nNote: If you for some reason need to change authentication profile name you can use environment variable `AWS_PROFILE` with\nname of the profile you wish to use.\n\nIf you don't want to save access key and secret access key into disk they can be\npassed to hedge by using environment variable `AWS_ACCESS_KEY_ID` and\n`AWS_SECRET_ACCESS_KEY`\n\nTo store environment variables for current shell session use command\n`export \u003cVARIABLE_NAME-1\u003e=\u003cVALUE_1\u003e \u003cVARIABLE_NAME-2\u003e=\u003cVALUE_2\u003e`\n\n### Supported Handler Types\n\n| **Handler type** | **Trigger**          | **Trigger Input**    | **Output**    | **Azure** | **AWS** |\n|------------------|----------------------|----------------------|---------------|-----------|---------|\n| :api             | HTTP Request         | HTTP Request         | HTTP Response | Yes       | Yes     |\n| :timer           | Cron schedule        | Timer                | result-\u003eJS    | Yes       | Yes     |\n| :queue           | New message in queue | Message              | result-\u003eJS    | Yes       | Yes     |\n\nOther handler types are planned.\n\nNotes on Azure handlers: In Azure you can configure the output to be passed as return value to other Azure services (i.e. queue, table storage etc).\n\n### Input/Output Bindings\n\nHedge can abstract Inputs and Outputs similar to Azure. A function can have a variable amount of input and output bindings.\nInputs are passed to the function on invocation and outputs are persisted on function successful complete (unless *nil*). Depending on configuration, you can use different implementations (if available) with same abstraction.\n\n| **type** | **Input**       | **Output**      | **Azure**        | **AWS** |\n|----------|-----------------|-----------------|------------------|---------|\n| :queue   |    n/a          |  Queue          | Storage Queue    |    TBA  |\n| :queue   |    n/a          |  Queue          | ServiceBus Queue |    TBA  |\n| :queue   |    n/a          |  Queue          | ServiceBus Topic |    TBA  |\n| :table   | Key-Value Store | Key-Value Store | Table Storage    |    TBA  |\n| :db      | Database        | Database        | CosmosDB         |    TBA  |\n\n### Logging\n\nLogging inside your serverless function is dependent on target platform. The Hedge handlers require internally [Timbre](https://github.com/ptaoussanis/timbre) as logging system.\nYou can use Timbre in your **handler** with following require:\n\nExample on handler that uses Timbre logging (in a file `src/my-cool-function/core.cljs`)\n\n    (ns my-cool-function.core\n        (:require [taoensso.timbre :as timbre\n                                   :refer [log  trace  debug  info  warn  error  fatal  report\n                                           logf tracef debugf infof warnf errorf fatalf reportf\n                                           spy get-env log-env]]))\n\n    ; default logging level is :debug\n    (timbre/set-level! :trace)\n\n    (defn handler [req]\n        (info \"hello info level\")\n        (error \"hello error level\"))\n\n### Basic Serverless Project Structure\n\nA basic structure can be found in one of the examples in [AWS example](https://github.com/jikuja/hedge-example-aws) repository.\n\nNote: Example directories might be merged to master repo at some point.\n\n* src/ - This directory contains your CLJS source files\n* target/ - Optionally persisted compiled output directory\n* resources/hedge.edn - Includes configuration information that maps your program function to the serverless function entry point. You can configure here if a function is protected with authentication code or available without authentication. Also function type is defined here (api, timer, ...)\n* test/ - This directory contains your unit tests\n* boot.properties - Boot properties\n* build.boot - Your build configuration file\n* package.json - Optionally, if you use external Node modules\n* node_modules - Optionally, if you have installed Node modules\n\nRefer example repositories for more info.\n\n### build.boot\n\n**build.boot** is boot configuration file. Hedge commands are implemented as boot task.\n\nExample configuration with Hedge and boot-cljs-test:\n\n```\n(set-env! :source-paths #{\"src\"}\n          :resource-paths #{\"resources\"}\n          :dependencies '[[org.clojure/clojurescript \"1.9.946\"]\n                          [adzerk/boot-cljs \"1.7.228-2\" :scope \"test\"]\n                          [crisptrutski/boot-cljs-test \"0.3.4\" :scope \"test\"]\n                          [siili/boot-hedge \"0.1.0\" :scope \"test\"]\n                          [siili/hedge \"0.1.0\"]])\n\n; refer example repository for detailed\n(require '[boot-hedge.core :as hedge])\n(hedge/hedge-init!)\n\n(require '[crisptrutski.boot-cljs-test :refer [test-cljs report-errors!] :as cljs-test])\n\n(deftask testing [] (set-env! :source-paths #(conj % \"test\")) identity)\n(ns-unmap 'boot.user 'test)\n\n(deftask test []\n  (comp (testing)\n        (test-cljs :js-env :node\n                   :exit?  true)))\n```\n\n### hedge.edn\n\n**resources/hedge.edn** contains the configuration for the given functions.\n\nExample of a configuration for two apis, two timers and one queue triggered function:\n\n```\n{:api {\"api1\" {:handler my_cool_function.core/crunch-my-data :authorization :anonymous}\n       \"api2\" {:handler my_cool_function.core/hello :authorization :function}}\n\n :timer {\"timer1\" {:handler my_cool_function.core/timer-handler :cron \"*/10 * * * *\"}\n         \"timer2\" {:handler my_cool_function.core/timer-handler-broken :cron \"*/10 * * * *\"}}\n\n :queue {:\"queue\" {:handler my_cool_function.core/timer-handler\n                   :queue \"nameOfQueue\"\n                   :connection \"environmental variable that holds the connection string (Azure)\"}}}\n```\n\nCommon structure:\n\n`{:handler-type {\"name-given-to-api\" {:handler namespace.core/name-of-function}}}`\n\nAzure **:api** specific:\n\n`:authorization :anonymous` or `:authorization :function` - defines if the HTTP endpoint public or access key protected\n\n**:timer** specific:\n`:cron \"{minutes} {hours} {day-of-month} {months} {day-of-week}\"`\n\nExample on cron expression that will trigger every minute: `\"*/1 * * * *\"`\n\nNote on used cron expression:\n- Only simple expressions are supported\n  - *, numbers and / wildcard\n  - setting both day-of-month and day-of-week is not supported (AWS limitation)\n  - names of days/months and L, W, ? and # wildcards are not supported\n- Azure has a field for seconds and AWS doesn't (field results to 0 when generating Azure function.json)\n- AWS has a field for years and Azure doesn't (field results to *)\n\nAzure users: You can modify the resulted function.json if you need to incorporate seconds in your cron expression\n\nAzure **:queue** specific:\n`:accessRights \"Manage\"` or `:accessRights \"Listen\"` - if defined, will use servicebus queue/topic queue instead of storage queue\n`:connection \"ENV_VARIABLE\"` - function app environmental variable that holds the connection string to the queue service (for example AzureWebJobsStorage for storage queues)\n`:subscription \"subscriptionName\"` - if defined together with `:accessRights`, will use a service bus topic \u0026 subscription. Subscription will be created if it does not exist.\n\nIf your function throws an unhandled exception or fails, the message is returned to the queue, retried and finally put into poison queue (azure storage queue) or dead-letter queue (servicebus queue).\n\nNote: It might be that it is not possible to put a servicebus topic message to dead-letter queue (when your function fails), because design decisions in Azure function runtime.  \n\nStorage Queue polling frequency and servicebus queue settings for the function app runtime can be set in **host.json**, see https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json#queues\n\nPlease note you must create the queues/topics yourself (you can do it through the portal or use Azure Storage Explorer with storage Queues).\n\n**Define Function Inputs and Outputs in hedge.edn:**\n\nPlease note **:connection** is Azure specific Function App Setting (env variable) that contains the connection string. In Azure you can specify target Queue implementation, see below.  \n\n```\n{:api {\"api1\" {:handler my_cool_function.core/crunch-my-data :authorization :anonymous}\n               :inputs [{:type :table\n                         :key \"in1\"\n                         :name \"inputTable\"\n                         :connection \"AzureWebJobStorage\"}\n                       {:type :db\n                         :key \"in2\"\n                         :name \"inputDb\"\n                         :collection \"collection\"\n                         :connection \"CosmosDBConnection\"}]\n               :outputs [{:type :queue\n                          :key \"out1\"\n                          :name \"queue\"\n                          :connection \"AzureWebJobsStorage\"}\n                         {:type :queue\n                          :key \"out2\"\n                          :accessRights \"Manage\"\n                          :name \"queue\"\n                          :connection \"SBQueueConnection\"}\n                         {:type :queue\n                          :key \"out3\"\n                          :topic true\n                          :accessRights \"Manage\"\n                          :name \"queue\"\n                          :connection \"SBTopicConnection\"}\n                         {:type :db\n                          :key \"out4\"\n                          :name \"db\"\n                          :collection \"collection\"\n                          :connection \"ConnectionString\"}\n                         {:type :table\n                          :key \"out5\"\n                          :name \"table\"\n                          :connection \"ConnectionString\"}]\n\n```\n\n### Handler signatures\n\nShort examples on handler signatures:\n\n```\n(defn api-handler\n    \"req contains the incoming http request, return value is passed as the response\"\n    [req]\n    \"Hello World!\")\n\n(defn timer-handler\n    \"timer contains the scheduled trigger timestamp, return can be passed to function output i.e. other wired service\"\n    [timer]\n    \"Hello World!\")\n\n(defn queue-handler\n    \"message contains the message payload, return can be passed to function output i.e. other wired service\"\n    [message]\n    \"Hello World!)\n```\n\nIf you are using inputs and outputs, you can currently use any of the following signature additions on any type of handler:\n\n```\n(defn api-handler-with-inputs\n    \"req contains the incoming http request, inputs contains a map of given inputs described in hedge.edn\"\n    [req \u0026 {:keys [inputs]}]\n    (info\n        \"Read table from table storage: \"\n        (-\u003e inputs :in1))\n    \"This goes as return value to HTTP response\")\n\n(defn api-handler-with-outputs\n    \"req contains the incoming http request, outputs contain a map of atoms for given outputs defined in hedge.edn\"\n    [req \u0026 {:keys [outputs]}]\n\n    ; write two messages to storage queue (mapped as :out1)\n    (reset! (-\u003e outputs :out1 :value) [{:message {:id \"1\" :content \"hello world\"}} {:message {:id \"2\" :content \"hello again\"}}])\n    ; one message to servicebus queue (Azure only)\n    (reset! (-\u003e outputs :out2 :value) {:message {:id \"1\" :content \"hello world\"}})\n    ; two messages to to servicebus topic (Azure only)\n    (reset! (-\u003e outputs :out3 :value) [{:message {:id \"1\" :content \"hello world\"}} {:message {:id \"2\" :content \"hello again\"}}])\n    ; to db (example with two rows)\n    (reset! (-\u003e outputs :out4 :value) [{:name \"John Doe\"\n                                        :address \"123 Hollywood\"}\n                                       {:name \"Jane Doe\"\n                                        :address \"123 Hollywood\"\n                                        :info \"available}])\n\n    ; write row to table storage (mapped as :out5)\n    (reset! (-\u003e outputs :out5 :value) {:PartitionKey \"partitionkey-1\" :RowKey \"rowkey-1 :value \"A value stored.\"})\n    \"This goes as return value to HTTP response\")\n\n(defn api-handler-with-inputs-and-outputs\n    [req \u0026 {:keys [inputs outputs]}]\n    ; as above\n    \"This goes as return value to HTTP response\")\n```\n\nSee examples for more usage patterns.\n\n### Exception Handling inside Serverless Function\n\nThese instructions especially apply for **Azure**, and is considered as best practice for **AWS** also in the context of Hedge.\n\nIf your function throws an uncaught exception, it will be passed as Error to the serverless runtime. This will be visible in Application Insights logs as user reported execution failure.\nThis is useful when for example receiving messages from a queue but your function would like to abort processing on failure and return the message to queue.\n\nIf you are using `go` blocks (i.e. performing async HTTP requests), throwing an uncaught exception (either accidentally or signaling the runtime on purpose) inside a `go` block will not be caught by the runtime and unexpected behavior can be expected. The solution is to use `go-try`:\n\nAdd dependency to your functions **build.boot** `[org.clojars.akiel/async-error \"0.2\"]` see https://github.com/alexanderkiel/async-error\n\nRequire the necessary macros in your handler `[async-error.core :refer-macros [go-try \u003c?]]`.\n\nInstead of using `go` and `\u003c!`, use `go-try` and `\u003c?`. Thus exceptions from port reading will be passed in the channels upstream and finally to the serverless runtime.\n\n### Testing\n\nTesting your function (unit testing) requires a JavaScript runtime, ie. Node.js (or PhantomJS) because ClojureScript is compiled to JavaScript and needs a platform to run on.\n\nTo run unit tests in test/ -folder\n\n    boot test\n\nTo run unit tests when files change in your project (watch project)\n\n    boot watch test\n\n### Deploying To Azure\n\nTo deploy, Hedge requires that you create your Resource Group. Hedge can create a Storage Account and Function App for you, but we recommend that you create these by your self so that you can choose where you host your serverless function code. One Function App can contain multiple serverless functions.\n\nWhen you create these by your self you can choose location of data center, storage options and service plan.\n\nStorage account is required to store your function code and logs that your functions create and other settings.\n\nYou can do it from the [Azure Portal](https://portal.azure.com) or use Azure CLI (Instructions below).\n\nExample create the Resource Group in northeurope data center:\n\n    az group create --name NameOfResourceGroup --location northeurope\n\nTo create your storage account with simplest storage option in north europe:\n\n    az storage account create --name NameOfStorageAccount --location northeurope --resource-group NameOfResourceGroup --sku Standard_LRS\n\nTo create your function app with consumption plan (Windows Server backed serverless runtime with dynamic resource scaling, no dedicated VM):\n\n    az functionapp create --name NameOfFunctionApp --storage-account NameOfStorageAccount --resource-group NameOfResourceGroup --consumption-plan-location northeurope\n\nTo compile and deploy your function to Azure:\n\n    boot azure/deploy -a NameOfFunctionApp -r NameOfResourceGroup\n\nIf your authentication file is correctly generated and found in the environment, your function should deploy to Azure and can be reached with HTTP.\n\n### Deploying To AWS\n\nTo compile and deploy your project to AWS:\n\n    boot aws/deploy -n \u003cSTACK_NAME\u003e\n\nCommand checks that `STACK_NAME` name is free. If it is free project\nis deployed into Cloudformation stack with given name. If name\nis reserved error message is shown. After build and deployment steps command print API endpoint base URL.\n\nVisible HTTP endpoints of the project are in base URL.\n\nTechnical note: command check if S3 bucket with name\n`hedge-\u003cSTACK_NAME\u003e` is free. This might be changed\nin the future.\n\n### Other Usage Examples\n\n    # Get information about the Azure Publishing Profile\n    boot azure/show-publish-profile -a functionapp -r resourcegroup\n\n    # Deploy to Azure and Persist the compiled artifacts in **target/** directory (index.js and function.json)\n    boot azure/deploy -a functionapp -r resourcegroup target\n\n    # Persist the compiled output. Given no options, defaults to Optimizations=simple and directory=target\n    boot azure/deploy-to-directory -O \u003coptimization level\u003e -f \u003cfunction name\u003e -d \u003cdirectory\u003e\n\n    # Deploy compiled artifacts from target directory (index.js and function.json)\n    boot azure/deploy-from-directory -a functionapp -r resourcegroup -d \u003cdirectory\u003e\n\n    # Get more help of task, i.e. commandline options\n    boot \u003ctask-name\u003e -h\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsiilisolutions%2Fhedge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsiilisolutions%2Fhedge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsiilisolutions%2Fhedge/lists"}