Source code for this article published on Medium.
- Install the newest stable version of Rust.
- Install the newest version of Go.
- Install the newest version of Ignite CLI.
- Clone this repository locally, to experiment with presented examples.
$ cargo install dsntk
$ dsntk --version
dsntk 0.0.4
The decision table for calculating SLA
is presented below. The source is saved in file sla.dtb.
This decision table is identical to the one presented in Haarmann's work.
βββββββββ
β SLA β
βββββ¬ββββ΄ββββββββββββββ¬ββββββββββββββββ₯ββββββ
β U β YearsAsCustomer β NumberOfUnits β SLA β
β βββββββββββββββββββΌββββββββββββββββ«ββββββ€
β β [0..100] β [0..1000000] β 1,2 β
βββββͺββββββββββββββββββͺββββββββββββββββ¬ββββββ‘
β 1 β <2 β <1000 β 1 β
βββββΌββββββββββββββββββΌββββββββββββββββ«ββββββ€
β 2 β <2 β >=1000 β 2 β
βββββΌββββββββββββββββββΌββββββββββββββββ«ββββββ€
β 3 β >=2 β <500 β 1 β
βββββΌββββββββββββββββββΌββββββββββββββββ«ββββββ€
β 4 β >=2 β >=500 β 2 β
βββββ΄ββββββββββββββββββ΄ββββββββββββββββ¨ββββββ
To evaluate this decision table, run:
$ dsntk edt sla.input sla.dtb
2
The sla.input file contains input data presented to decision table during evaluation.
To test this decision table, run:
$ dsntk tdt sla.test sla.dtb
test 1 ... ok
test 2 ... ok
test 3 ... ok
test 4 ... ok
test 5 ... ok
test 6 ... ok
test 7 ... ok
test 8 ... ok
test 9 ... ok
test 10 ... ok
test 11 ... ok
test result: ok. 11 passed; 0 failed.
The decision table for calculating Fine
is presented below. The source is saved in file fine.dtb.
This decision table is identical to the one presented in Haarmann's work.
βββββββββ
β Fine β
βββββ¬ββββ΄βββββββββββββ¬ββββββ₯βββββββ
β U β DefectiveUnits β SLA β Fine β
β ββββββββββββββββββΌββββββ«βββββββ€
β β [0.00..1.00] β 1,2 β β
βββββͺβββββββββββββββββͺββββββ¬βββββββ‘
β 1 β < 0.05 β 1 β 0.00 β
βββββΌβββββββββββββββββΌββββββ«βββββββ€
β 2 β [0.05..0.10] β 1 β 0.02 β
βββββΌβββββββββββββββββΌββββββ«βββββββ€
β 3 β > 0.10 β 1 β 1.00 β
βββββΌβββββββββββββββββΌββββββ«βββββββ€
β 4 β < 0.01 β 2 β 0.00 β
βββββΌβββββββββββββββββΌββββββ«βββββββ€
β 5 β [0.01..0.05] β 2 β 0.05 β
βββββΌβββββββββββββββββΌββββββ«βββββββ€
β 6 β > 0.05 β 2 β 1.05 β
βββββ΄βββββββββββββββββ΄ββββββ¨βββββββ
To evaluate this decision table, run:
$ dsntk edt fine.input fine.dtb
0.02
The fine.input file contains input data presented to decision table during evaluation.
To test this decision table, run:
test 1 ... ok
test 2 ... ok
test 3 ... ok
test 4 ... ok
test 5 ... ok
test 6 ... ok
test 7 ... ok
test 8 ... ok
test 9 ... ok
test 10 ... ok
test 11 ... ok
test 12 ... ok
test result: ok. 12 passed; 0 failed.
Decision tables presented above contain properly working decision logic as described in Haarmann's work. This logic must be combined in a decision model, specifying requirements and dependencies as depicted below:
Decision model is identical to the one presented in Haarmann's work. Every decision model, to be evaluated, must be prepared in XML format, compliant with DMN specification. The file mancus.dmn contains such a model. The content of this file is presented below.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<definitions namespace="https://dsntk.io"
name="DecisionContract"
id="_f78964ab-4b04-4dee-b9b0-fa3db9b2e499"
xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/"
xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/"
xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/">
<description>
Decision contract for calculating the _fine_.
</description>
<decision name="SLA" label="SLA" id="_822e095e-a12e-4de4-9468-14c059e354c3">
<description>
Calculates the **SLA**.
</description>
<variable typeRef="number" name="SLA">
<description>
Calculated SLA.
</description>
</variable>
<informationRequirement id="_a5c2170c-8187-43f6-9a70-53ad64a8446b">
<requiredInput href="#_32873537-d1f7-4305-9d2f-6b1b0ab91dc1"/>
</informationRequirement>
<informationRequirement id="_9e70e348-4e66-485d-8dc4-6a16cc65fa05">
<requiredInput href="#_dd4cf4f2-92a4-4f97-96f3-0458c3c32d25"/>
</informationRequirement>
<decisionTable outputLabel="SLA">
<input>
<inputExpression typeRef="number">
<text>YearsAsCustomer</text>
</inputExpression>
<inputValues>
<text>[0..100]</text>
</inputValues>
</input>
<input>
<inputExpression typeRef="number">
<text>NumberOfUnits</text>
</inputExpression>
<inputValues>
<text>[0..1000000]</text>
</inputValues>
</input>
<output>
<outputValues>
<text>1,2</text>
</outputValues>
</output>
<rule>
<inputEntry>
<text>< 2</text>
</inputEntry>
<inputEntry>
<text>< 1000</text>
</inputEntry>
<outputEntry>
<text>1</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>< 2</text>
</inputEntry>
<inputEntry>
<text>>= 1000</text>
</inputEntry>
<outputEntry>
<text>2</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>>= 2</text>
</inputEntry>
<inputEntry>
<text>< 500</text>
</inputEntry>
<outputEntry>
<text>1</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>>= 2</text>
</inputEntry>
<inputEntry>
<text>>= 500</text>
</inputEntry>
<outputEntry>
<text>2</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<decision name="Fine" label="Fine" id="_77a97976-3140-4a91-9b47-c3d3587f3065">
<description>
Calculates the **fine**.
</description>
<variable typeRef="number" name="Fine">
<description>
Calculated fine.
</description>
</variable>
<informationRequirement id="_738f8936-85ac-4f8c-9bc2-b2e2ed9e1f80">
<requiredInput href="#_ab93cef8-48c2-4c79-9165-12531c4a4b3f"/>
</informationRequirement>
<informationRequirement id="_6e20677f-f7c1-4000-9acb-b063fa35af16">
<requiredDecision href="#_822e095e-a12e-4de4-9468-14c059e354c3"/>
</informationRequirement>
<decisionTable outputLabel="Fine">
<input>
<inputExpression typeRef="number">
<text>DefectiveUnits</text>
</inputExpression>
<inputValues>
<text>[0.00 .. 1.00]</text>
</inputValues>
</input>
<input>
<inputExpression typeRef="number">
<text>SLA</text>
</inputExpression>
<inputValues>
<text>1,2</text>
</inputValues>
</input>
<output/>
<rule>
<inputEntry>
<text>< 0.05</text>
</inputEntry>
<inputEntry>
<text>1</text>
</inputEntry>
<outputEntry>
<text>0</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>[0.05 .. 0.1]</text>
</inputEntry>
<inputEntry>
<text>1</text>
</inputEntry>
<outputEntry>
<text>0.02</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>> 0.1</text>
</inputEntry>
<inputEntry>
<text>1</text>
</inputEntry>
<outputEntry>
<text>1</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>< 0.01</text>
</inputEntry>
<inputEntry>
<text>2</text>
</inputEntry>
<outputEntry>
<text>0</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>[0.01 .. 0.05]</text>
</inputEntry>
<inputEntry>
<text>2</text>
</inputEntry>
<outputEntry>
<text>0.05</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>> 0.05</text>
</inputEntry>
<inputEntry>
<text>2</text>
</inputEntry>
<outputEntry>
<text>1.05</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<inputData name="YearsAsCustomer" label="years as customer" id="_32873537-d1f7-4305-9d2f-6b1b0ab91dc1">
<variable typeRef="number" name="YearsAsCustomer">
<description>
Number of years the customer buys units from the manufacturer.
**Value provided by the manufacturer.**
</description>
</variable>
</inputData>
<inputData name="NumberOfUnits" label="number of units" id="_dd4cf4f2-92a4-4f97-96f3-0458c3c32d25">
<variable typeRef="number" name="NumberOfUnits">
<description>
Total number of units bought by the customer during whole cooperation with the manufacturer.
**Value provided by the manufacturer.**
</description>
</variable>
</inputData>
<inputData name="DefectiveUnits" label="defective units" id="_ab93cef8-48c2-4c79-9165-12531c4a4b3f">
<variable typeRef="number" name="DefectiveUnits">
<description>
Number of defective units.
**Value provided by the customer.**
</description>
</variable>
</inputData>
<dmndi:DMNDI>
<dmndi:DMNDiagram sharedStyle="style1">
<dmndi:Size height="340.0" width="680.0"/>
<dmndi:DMNShape dmnElementRef="_822e095e-a12e-4de4-9468-14c059e354c3">
<dc:Bounds height="80.0" width="100.0" x="200.0" y="60.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_77a97976-3140-4a91-9b47-c3d3587f3065">
<dc:Bounds height="80.0" width="100.0" x="470.0" y="60.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_32873537-d1f7-4305-9d2f-6b1b0ab91dc1">
<dc:Bounds height="60.0" width="160.0" x="80.0" y="220.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_dd4cf4f2-92a4-4f97-96f3-0458c3c32d25">
<dc:Bounds height="60.0" width="160.0" x="260.0" y="220.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_ab93cef8-48c2-4c79-9165-12531c4a4b3f" sharedStyle="style2">
<dc:Bounds height="60.0" width="160.0" x="440.0" y="220.0"/>
</dmndi:DMNShape>
<dmndi:DMNEdge dmnElementRef="_a5c2170c-8187-43f6-9a70-53ad64a8446b">
<di:waypoint x="160.0" y="220.0"/>
<di:waypoint x="230.0" y="140.0"/>
</dmndi:DMNEdge>
<dmndi:DMNEdge dmnElementRef="_9e70e348-4e66-485d-8dc4-6a16cc65fa05">
<di:waypoint x="340.0" y="220.0"/>
<di:waypoint x="270.0" y="140.0"/>
</dmndi:DMNEdge>
<dmndi:DMNEdge dmnElementRef="_738f8936-85ac-4f8c-9bc2-b2e2ed9e1f80">
<di:waypoint x="520.0" y="220.0"/>
<di:waypoint x="520.0" y="140.0"/>
</dmndi:DMNEdge>
<dmndi:DMNEdge dmnElementRef="_6e20677f-f7c1-4000-9acb-b063fa35af16">
<di:waypoint x="300.0" y="100.0"/>
<di:waypoint x="470.0" y="100.0"/>
</dmndi:DMNEdge>
</dmndi:DMNDiagram>
<dmndi:DMNStyle id="style1" fontSize="12"/>
<dmndi:DMNStyle id="style2">
<dmndi:FillColor red="220" green="220" blue="220"/>
</dmndi:DMNStyle>
</dmndi:DMNDI>
</definitions>
To execute the decision model using DSNTK, run:
$ dsntk srv -v
Found 1 model.
Loaded 1 model.
Deployed 2 invocables.
Deployed invocables:
io/dsntk/DecisionContract/Fine
io/dsntk/DecisionContract/SLA
dsntk 0.0.0.0:22022
DSNTK, run with command srv
searches for DMN models in current directory. While there is exactly one in file
mancus.dmn, DSNTK loads the model and deploys invocables (decision tables in our case) and prepares
JSON API endpoints to evaluate those invocables.
To test SLA
decision, open another terminal and run:
$ curl -s \
-d "{ YearsAsCustomer: 1, NumberOfUnits: 1000 }" \
-H "Content-Type: application/json" \
-X POST http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/SLA
{"data":2}
The result SLA
is 2.
To test Fine
decision, run:
$ curl -s \
-d "{ YearsAsCustomer: 1, NumberOfUnits: 1000, DefectiveUnits: 0.034 }" \
-H "Content-Type: application/json" \
-X POST http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/Fine
{"data":0.05}
The result Fine
is 0.05 that is 5%.
To run both tests shown above:
$ chmod +x mancus.sh
$ ./mancus.sh
Calculating SLA:
{"data":2}
Calculating fine:
{"data":0.05}
Now we have the DMN decision model up and running. This model can be evaluated by executing two JSON API endpoints using
curl
:
The example Go application that evaluates decision model is prepared in file client/main.go. The source is presented below:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const Uri = "http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/"
const SlaUri = Uri + "SLA"
const FineUri = Uri + "Fine"
const ContentType = "application/json"
type SlaParams struct {
YearsAsCustomer int64 `json:"YearsAsCustomer"`
NumberOfUnits int64 `json:"NumberOfUnits"`
}
type SlaResult struct {
Data int64 `json:"data"`
}
type FineParams struct {
YearsAsCustomer int64 `json:"YearsAsCustomer"`
NumberOfUnits int64 `json:"NumberOfUnits"`
DefectiveUnits float64 `json:"DefectiveUnits"`
}
type FineResult struct {
Data float64 `json:"data"`
}
func querySla(yearsAsCustomer int64, numberOfUnits int64) int64 {
slaParams := SlaParams{
YearsAsCustomer: yearsAsCustomer,
NumberOfUnits: numberOfUnits,
}
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(&slaParams)
if err != nil {
panic(err)
}
response, err := http.Post(SlaUri, ContentType, &body)
if err != nil {
panic(err)
}
slaResult := SlaResult{}
err = json.NewDecoder(response.Body).Decode(&slaResult)
if err != nil {
panic(err)
}
return slaResult.Data
}
func queryFine(yearsAsCustomer int64, numberOfUnits int64, defectiveUnits float64) float64 {
fineParams := FineParams{
YearsAsCustomer: yearsAsCustomer,
NumberOfUnits: numberOfUnits,
DefectiveUnits: defectiveUnits,
}
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(&fineParams)
if err != nil {
panic(err)
}
response, err := http.Post(FineUri, ContentType, &body)
if err != nil {
panic(err)
}
fineResult := FineResult{}
err = json.NewDecoder(response.Body).Decode(&fineResult)
if err != nil {
panic(err)
}
return fineResult.Data
}
func main() {
fmt.Printf("SLA = %d\n", querySla(1, 1000))
fmt.Printf("Fine = %.0f%%\n", queryFine(1, 1000, 0.034)*100)
}
To test this application, run:
$ cd client
$ go run dsntk/client
SLA = 2
Fine = 5%
NOTE: The DSNTK server must be running with decision model deployed, but this is obvious ;-)
Now we have the DMN decision model up and running. this model can be executed from Go application. We will use this Go code to implement query in Cosmos blockchain.
Check ignite version:
$ ignite ignite version
Ignite CLI version: v28.1.1
Cosmos SDK version: v0.50.3
Your OS: linux
Your arch: amd64
Your Node.js version: v20.10.0
Your go version: go version go1.21.6 linux/amd64
Is on Gitpod: false
Create a chain named decon
with custom module named decon
:
$ ignite scaffold chain decon
Create a custom query named sla
:
$ cd decon
$ ignite scaffold query sla yearsAsCustomer:uint numberOfUnits:uint --response sla:uint
modify proto/decon/decon/query.proto
create x/decon/keeper/query_sla.go
modify x/decon/module/autocli.go
π Created a query `sla`.
Create a custom query named fine
:
$ ignite scaffold query fine yearsAsCustomer:uint numberOfUnits:uint defectiveUnits:uint --response fine:uint
modify proto/decon/decon/query.proto
create x/decon/keeper/query_fine.go
modify x/decon/module/autocli.go
π Created a query `fine`.
This file was added from Go client and slightly modified: x/decon/keeper/dsntk_client.go
package keeper
import (
"bytes"
"encoding/json"
"net/http"
)
const Uri = "http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/"
const SlaUri = Uri + "SLA"
const FineUri = Uri + "Fine"
const ContentType = "application/json"
const Multiplier = 100000000.0
type SlaParams struct {
YearsAsCustomer uint64 `json:"YearsAsCustomer"`
NumberOfUnits uint64 `json:"NumberOfUnits"`
}
type SlaResult struct {
Data uint64 `json:"data"`
}
type FineParams struct {
YearsAsCustomer uint64 `json:"YearsAsCustomer"`
NumberOfUnits uint64 `json:"NumberOfUnits"`
DefectiveUnits float64 `json:"DefectiveUnits"`
}
type FineResult struct {
Data float64 `json:"data"`
}
func querySla(yearsAsCustomer uint64, numberOfUnits uint64) uint64 {
slaParams := SlaParams{
YearsAsCustomer: yearsAsCustomer,
NumberOfUnits: numberOfUnits,
}
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(&slaParams)
if err != nil {
panic(err)
}
response, err := http.Post(SlaUri, ContentType, &body)
if err != nil {
panic(err)
}
slaResult := SlaResult{}
err = json.NewDecoder(response.Body).Decode(&slaResult)
if err != nil {
panic(err)
}
return slaResult.Data
}
func queryFine(yearsAsCustomer uint64, numberOfUnits uint64, defectiveUnits uint64) uint64 {
fineParams := FineParams{
YearsAsCustomer: yearsAsCustomer,
NumberOfUnits: numberOfUnits,
DefectiveUnits: float64(defectiveUnits) / Multiplier,
}
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(&fineParams)
if err != nil {
panic(err)
}
response, err := http.Post(FineUri, ContentType, &body)
if err != nil {
panic(err)
}
fineResult := FineResult{}
err = json.NewDecoder(response.Body).Decode(&fineResult)
if err != nil {
panic(err)
}
return uint64(fineResult.Data * Multiplier)
}
This file is modified: x/decon/keeper/query_sla.go
package keeper
import (
"context"
"decon/x/decon/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (k Keeper) Sla(goCtx context.Context, req *types.QuerySlaRequest) (*types.QuerySlaResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(goCtx)
// TODO: Process the query
_ = ctx
sla := querySla(req.YearsAsCustomer, req.NumberOfUnits)
return &types.QuerySlaResponse{Sla: sla}, nil
}
This file is modified: x/decon/keeper/query_fine.go
package keeper
import (
"context"
"decon/x/decon/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (k Keeper) Fine(goCtx context.Context, req *types.QueryFineRequest) (*types.QueryFineResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(goCtx)
// TODO: Process the query
_ = ctx
fine := queryFine(req.YearsAsCustomer, req.NumberOfUnits, req.DefectiveUnits)
return &types.QueryFineResponse{Fine: fine}, nil
}
Start DSNTK server in one terminal (if not already started):
$ dsntk srv
Start the chain (in second terminal:
$ ignite chain serve
Query SLA
(in third terminal):
$ ~/go/bin/decond query decon sla 1 1000
sla: "2"
Query Fine
(in fourth terminal):
$ ~/go/bin/decond query decon fine 1 1000 3400000
fine: "5000000"
Now we have a chain with custom module, that executes DMN decision model.