-
Notifications
You must be signed in to change notification settings - Fork 4
/
working_memory_data_model.cljc
187 lines (167 loc) · 7.48 KB
/
working_memory_data_model.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
(ns com.fulcrologic.statecharts.data-model.working-memory-data-model
"An implementation of DataModel that stores the data in working memory itself.
Supports using `src` in data model for CLJ ONLY, which must be a URI that clojure.java.io/reader
would accept.
There are two implementations: One where data is scoped to the state, and another where it is global.
"
(:require
#?(:clj
[clojure.edn :as edn])
[com.fulcrologic.statecharts :as sc]
[com.fulcrologic.statecharts.environment :as env]
[com.fulcrologic.statecharts.protocols :as sp]
[com.fulcrologic.statecharts.chart :as chart]
[taoensso.timbre :as log]))
(defmulti run-op (fn [all-data context-id {:keys [op]}] op))
(defmethod run-op :default [all-data context-id op]
(log/warn "Operation not understood" op)
all-data)
(defmethod run-op :assign [all-data context-id {:keys [data]}]
(reduce-kv
(fn [acc path value]
(cond
(and context-id (keyword? path))
(do
(log/trace "Assigning" value "to" [context-id path])
(assoc-in acc [context-id path] value))
(keyword? path)
(do
(log/error "Internal error: Unknown context for assignment to" path)
acc)
(and (vector? path) (= (count path) 2))
(do
(log/trace "Assigning" value "to" path)
(assoc-in acc path value))
:else (do
(log/warn "Cannot assign value. Illegal path expression" path)
acc)))
all-data
data))
(defmethod run-op :delete [all-data context-id {:keys [paths]}]
(reduce
(fn [M path]
(cond
(and context-id (keyword? path)) (update M context-id dissoc path)
(and (vector? path) (= (count path) 2)) (update M (first path) dissoc (second path))
:else (do
(log/warn "Cannot delete value. Illegal path expression" path)
M)))
all-data
paths))
(deftype WorkingMemoryDataModel []
sp/DataModel
(load-data [provider {::sc/keys [vwmem] :as env} src]
#?(:clj (try
(let [data (edn/read-string (slurp src))
state-id (or (env/context-element-id env) :ROOT)]
(if (map? data)
(do
(log/trace "Loaded" data "into context" state-id)
(vswap! vwmem ::data-model assoc state-id data))
(log/error "Unable to use loaded data from" src "because it is not a map.")))
(catch #?(:clj Throwable :cljs :default) e
(log/error e "Unable to load data from" src)))
:cljs (log/error "src not supported.")))
(current-data [_ {::sc/keys [statechart vwmem] :as env}]
(let [all-data (some-> vwmem deref ::data-model)]
(loop [state-id (env/context-element-id env)
result {}]
(let [result (merge (get all-data state-id) result)
parent (chart/get-parent statechart state-id)]
(if (or (nil? parent) (= :ROOT parent))
(merge (get all-data :ROOT) result)
(recur parent result))))))
(get-at [provider env path]
(when (or (keyword? path) (vector? path))
(let [all-data (sp/current-data provider env)]
(get all-data (if (keyword? path) path (last path))))))
(update! [provider {::sc/keys [statechart vwmem] :as env} {:keys [ops] :as args}]
(when-not (map? args)
(log/error "You forgot to wrap your operations in a map!" args))
(let [all-data (some-> vwmem deref ::data-model)
state-id (env/context-element-id env)
new-data (reduce (fn [acc op] (run-op acc state-id op)) all-data ops)]
(vswap! vwmem assoc ::data-model new-data))))
(defn new-model
"Creates a data model where data is stored in the working memory of the state machine.
The data is scoped to the state it is declared or set in (visible to states
below it). Locations in this data model are [state-id key], where the special state-id :ROOT stands
for the top-level machine scope. Using a keyword as a location is resolved relative to the current
state, then parent, parent parent, etc.
`current-data` is a merge of all data for the contextual state from root, with each nested state overriding anything
that appeared in a parent state.
`get-at` will NOT walk scopes, but supports simple keywords for the current context,
and paths of the form `[state-id data-key]`. The special state-id `:ROOT` is reserved for those at the top-most level.
The operations implemented for this model can be extended by adding to the multimethod `run-op`.
WARNING: This model is not recommended for many use-cases. The contextual paths turn out to be
rather difficult to reason about. The flat data model is recommended."
[]
(->WorkingMemoryDataModel))
(defmulti run-flat-op (fn [data {:keys [op]}] op))
(defmethod run-flat-op :default [all-data op]
(log/warn "Operation not understood" op)
all-data)
(defmethod run-flat-op :assign [all-data {:keys [data]}]
(reduce-kv
(fn [acc path value]
(cond
(= :ROOT path) value
(keyword? path) (assoc acc path value)
(and (vector? path) (= :ROOT (first path))) (assoc-in acc (rest path) value)
(vector? path) (assoc-in acc path value)
:else acc))
all-data
data))
(defn- dissoc-in [m ks]
(cond
(empty? ks) m
(= 1 (count ks)) (dissoc m (first ks))
(contains? (get-in m (butlast ks)) (last ks)) (update-in m (butlast ks) dissoc (last ks))
:else m))
(defmethod run-flat-op :delete [all-data {:keys [paths]}]
(reduce
(fn [M path]
(cond
(= :ROOT path) {}
(keyword? path) (dissoc M path)
(and (vector? path) (= :ROOT (first path))) (dissoc-in M (rest path))
(vector? path) (dissoc-in M path)
:else M))
all-data
paths))
(deftype FlatWorkingMemoryDataModel []
sp/DataModel
(load-data [provider {::sc/keys [vwmem] :as env} src]
#?(:clj (try
(let [data (edn/read-string (slurp src))
state-id (or (env/context-element-id env) :ROOT)]
(if (map? data)
(do
(log/trace "Loaded" data "into context" state-id)
(vswap! vwmem ::data-model assoc state-id data))
(log/error "Unable to use loaded data from" src "because it is not a map.")))
(catch #?(:clj Throwable :cljs :default) e
(log/error e "Unable to load data from" src)))
:cljs (log/error "src not supported.")))
(current-data [_ {::sc/keys [vwmem]}] (some-> vwmem deref ::data-model))
(get-at [provider env path]
(let [data (sp/current-data provider env)]
(cond
(= :ROOT path) data
(keyword? path) (get data path)
(and (vector? path) (= :ROOT (first path))) (get-in data (rest path))
(vector? path) (get-in data path)
:else nil)))
(update! [provider {::sc/keys [statechart vwmem] :as env} {:keys [ops] :as args}]
(when-not (map? args)
(log/error "You forgot to wrap your operations in a map!" args))
(let [all-data (some-> vwmem deref ::data-model)
new-data (reduce (fn [acc op] (run-flat-op acc op)) all-data ops)]
(vswap! vwmem assoc ::data-model new-data))))
(defn new-flat-model
"Creates a data model where data is stored in the working memory of the state machine.
ALL data scoped to a single map. Location paths work like get-in and assoc-in on that map. The special location
path `:ROOT` is simply ignored. The keys [:ROOT :a] === [:a] === :a
"
[]
(->FlatWorkingMemoryDataModel))