-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHaystackServer.swift
More file actions
250 lines (225 loc) · 9.6 KB
/
HaystackServer.swift
File metadata and controls
250 lines (225 loc) · 9.6 KB
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import Foundation
import Haystack
/// A HaystackServer is a server that implements the Haystack API.
/// It translates API calls into operations on the underlying data stores.
public final class HaystackServer: API, Sendable {
let recordStore: RecordStore
let historyStore: HistoryStore
let watchStore: WatchStore
let navPath: [String]
let onInvokeAction: @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid
let onEval: @Sendable (String) async throws -> Haystack.Grid
public init(
recordStore: RecordStore,
historyStore: HistoryStore,
watchStore: WatchStore,
navPath: [String] = ["site", "equip", "point"],
onInvokeAction: @escaping @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in
GridBuilder().toGrid()
},
onEval: @escaping @Sendable (String) async throws -> Haystack.Grid = { _ in
GridBuilder().toGrid()
}
) {
self.recordStore = recordStore
self.historyStore = historyStore
self.watchStore = watchStore
self.navPath = navPath
self.onInvokeAction = onInvokeAction
self.onEval = onEval
}
public func close() async throws {}
public func about() async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
try gb.addRow([
"haystackVersion": "4.0",
"tz": "New_York",
"serverName": "Test Server",
"serverTime": DateTime(date: Foundation.Date()),
"serverBootTime": DateTime(date: Foundation.Date() - ProcessInfo.processInfo.systemUptime),
"productName": "swift-haystack",
"productUri": Uri("https://github.com/NeedleInAJayStack/swift-haystack"),
"productVersion": "0.0.0", // TODO: Version
"vendorName": "NeedleInAJayStack",
"vendorUri": Uri("https://github.com/NeedleInAJayStack"),
])
return gb.toGrid()
}
public func defs(filter: String?, limit: Haystack.Number?) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
var queryFilter = "def"
if let filter = filter {
queryFilter += " and (\(filter))"
}
let dicts = try await recordStore.read(filter: queryFilter, limit: limit)
try gb.addRows(dicts)
return gb.toGrid()
}
public func libs(filter: String?, limit: Haystack.Number?) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
var queryFilter = "lib"
if let filter = filter {
queryFilter += " and (\(filter))"
}
let dicts = try await recordStore.read(filter: queryFilter, limit: limit)
try gb.addRows(dicts)
return gb.toGrid()
}
public func ops(filter: String?, limit: Haystack.Number?) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
var queryFilter = "def and op"
if let filter = filter {
queryFilter += " and (\(filter))"
}
let dicts = try await recordStore.read(filter: queryFilter, limit: limit)
try gb.addRows(dicts)
return gb.toGrid()
}
public func filetypes(filter: String?, limit: Haystack.Number?) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
var queryFilter = "def and filetype"
if let filter = filter {
queryFilter += " and (\(filter))"
}
let dicts = try await recordStore.read(filter: queryFilter, limit: limit)
try gb.addRows(dicts)
return gb.toGrid()
}
public func read(ids: [Haystack.Ref]) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
let dicts = try await recordStore.read(ids: ids)
try gb.addRows(dicts)
return gb.toGrid()
}
public func read(filter: String, limit: Haystack.Number?) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
let dicts = try await recordStore.read(filter: filter, limit: limit)
try gb.addRows(dicts)
return gb.toGrid()
}
public func nav(navId parentId: Haystack.Ref?) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
try gb.addCol(name: "navId")
guard navPath.count > 0 else {
return gb.toGrid()
}
guard let parentId = parentId else {
// If no input, just return the first level of navigation
for result in try await recordStore.read(filter: "\(navPath[0])", limit: nil) {
var navResult = result
navResult["navId"] = result["id"]
try gb.addRow(navResult)
}
return gb.toGrid()
}
guard let parentDict = try await recordStore.read(ids: [parentId]).first else {
throw ServerError.idNotFound(parentId)
}
// Find the first component of the navPath that matches a tag on the input dict
var parentNavPathIndex: Int? = nil
for index in 0 ..< navPath.count {
let component = navPath[index]
if parentDict.has(component) {
parentNavPathIndex = index
}
}
guard let parentNavPathIndex = parentNavPathIndex else {
throw ServerError.navPathComponentNotFound(navPath)
}
guard parentNavPathIndex < navPath.count - 1 else {
// Parent is a navPath leaf. No further navigation is possible, so return nothing.
return gb.toGrid()
}
let parentNavComponent = navPath[parentNavPathIndex]
let childNavComponent = navPath[parentNavPathIndex + 1]
// Read children using child component and inferring parent ref tag
let children = try await recordStore.read(
filter: "\(childNavComponent) and \(parentNavComponent)Ref == \(parentId.toZinc())",
limit: nil
)
for child in children {
var navChild = child
navChild["navId"] = child["id"]
try gb.addRow(navChild)
}
return gb.toGrid()
}
public func hisRead(id: Haystack.Ref, range: Haystack.HisReadRange) async throws -> Haystack.Grid {
let gb = Haystack.GridBuilder()
try gb.addCol(name: "ts")
try gb.addCol(name: "val")
let dicts = try await historyStore.hisRead(id: id, range: range)
try gb.addRows(dicts)
return gb.toGrid()
}
public func hisWrite(id: Haystack.Ref, items: [Haystack.HisItem]) async throws -> Haystack.Grid {
try await historyStore.hisWrite(id: id, items: items)
return GridBuilder().toGrid()
}
public func pointWrite(id _: Haystack.Ref, level _: Haystack.Number, val _: any Haystack.Val, who _: String?, duration _: Haystack.Number?) async throws -> Haystack.Grid {
// TODO: Implement
return GridBuilder().toGrid()
}
public func pointWriteStatus(id _: Haystack.Ref) async throws -> Haystack.Grid {
// TODO: Implement
return GridBuilder().toGrid()
}
public func watchSubCreate(watchDis _: String, lease: Haystack.Number?, ids: [Haystack.Ref]) async throws -> Haystack.Grid {
let watchId = try await watchStore.create(ids: ids, lease: lease)
let builder = GridBuilder().setMeta([
"watchId": watchId,
"lease": lease ?? Haystack.Null.val,
])
let watchRecs = try await recordStore.read(ids: ids)
try builder.addRows(watchRecs)
try await watchStore.updateLastReported(watchId: watchId)
return builder.toGrid()
}
public func watchSubAdd(watchId: String, lease: Haystack.Number?, ids: [Haystack.Ref]) async throws -> Haystack.Grid {
try await watchStore.addIds(watchId: watchId, ids: ids)
let builder = GridBuilder().setMeta([
"watchId": watchId,
"lease": lease ?? Haystack.Null.val,
])
let watchRecs = try await recordStore.read(ids: ids)
try builder.addRows(watchRecs)
try await watchStore.updateLastReported(watchId: watchId)
return builder.toGrid()
}
public func watchUnsubRemove(watchId: String, ids: [Haystack.Ref]) async throws -> Haystack.Grid {
try await watchStore.removeIds(watchId: watchId, ids: ids)
return GridBuilder().toGrid()
}
public func watchUnsubDelete(watchId: String) async throws -> Haystack.Grid {
try await watchStore.delete(watchId: watchId)
return GridBuilder().toGrid()
}
public func watchPoll(watchId: String, refresh: Bool) async throws -> Haystack.Grid {
let watch = try await watchStore.read(watchId: watchId)
let builder = GridBuilder().setMeta([
"watchId": watchId,
])
var watchRecs = [Dict]()
if refresh {
watchRecs = try await recordStore.read(ids: watch.ids)
} else {
watchRecs = try await recordStore.read(ids: watch.ids).filter { rec in
try rec.trap("mod", as: DateTime.self).date > watch.lastReported ?? .distantPast
}
}
try builder.addRows(watchRecs)
try await watchStore.updateLastReported(watchId: watchId)
return builder.toGrid()
}
public func invokeAction(id: Haystack.Ref, action: String, args: [String: any Haystack.Val]) async throws -> Haystack.Grid {
return try await onInvokeAction(id, action, args)
}
public func eval(expression: String) async throws -> Haystack.Grid {
return try await onEval(expression)
}
}
public enum ServerError: Error {
case idNotFound(Haystack.Ref)
case navPathComponentNotFound([String])
case watchNotFound(String)
}