@voxgig/sdkgen 4.31.1 → 4.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/helpers/definitionPlan.d.ts +5 -0
  3. package/dist/helpers/definitionPlan.js +44 -7
  4. package/dist/helpers/definitionPlan.js.map +1 -1
  5. package/dist/tsconfig.tsbuildinfo +1 -1
  6. package/package.json +1 -1
  7. package/project/.sdk/tm/c/utility/make_url.c +11 -0
  8. package/project/.sdk/tm/c/utility/prepare_headers.c +53 -3
  9. package/project/.sdk/tm/c/utility/prepare_query.c +4 -1
  10. package/project/.sdk/tm/c/utility/transform_request.c +31 -8
  11. package/project/.sdk/tm/clojure/src/sdk/core.clj +62 -15
  12. package/project/.sdk/tm/cpp/utility/pipeline.hpp +72 -15
  13. package/project/.sdk/tm/csharp/utility/MakeUrl.cs +8 -0
  14. package/project/.sdk/tm/csharp/utility/PrepareHeaders.cs +34 -4
  15. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +14 -0
  16. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +31 -5
  17. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +86 -24
  18. package/project/.sdk/tm/go/utility/make_url.go +8 -0
  19. package/project/.sdk/tm/go/utility/prepare_headers.go +36 -7
  20. package/project/.sdk/tm/go/utility/prepare_query.go +11 -0
  21. package/project/.sdk/tm/go/utility/transform_request.go +31 -5
  22. package/project/.sdk/tm/java/utility/MakeUrl.java +8 -0
  23. package/project/.sdk/tm/java/utility/PrepareHeaders.java +30 -6
  24. package/project/.sdk/tm/java/utility/PrepareQuery.java +10 -0
  25. package/project/.sdk/tm/java/utility/TransformRequest.java +30 -5
  26. package/project/.sdk/tm/js/src/utility/MakeUrlUtility.js +7 -0
  27. package/project/.sdk/tm/js/src/utility/PrepareHeadersUtility.js +18 -0
  28. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +3 -1
  29. package/project/.sdk/tm/js/src/utility/TransformRequestUtility.js +17 -3
  30. package/project/.sdk/tm/js/test/definition-runner.js +74 -13
  31. package/project/.sdk/tm/kotlin/utility/MakeSpecUrl.kt +8 -0
  32. package/project/.sdk/tm/kotlin/utility/Prepare.kt +34 -4
  33. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -6
  34. package/project/.sdk/tm/lua/utility/make_url.lua +8 -0
  35. package/project/.sdk/tm/lua/utility/prepare_headers.lua +35 -6
  36. package/project/.sdk/tm/lua/utility/prepare_query.lua +10 -0
  37. package/project/.sdk/tm/lua/utility/transform_request.lua +37 -6
  38. package/project/.sdk/tm/ocaml/sdk_runtime.ml +63 -13
  39. package/project/.sdk/tm/perl/utility/make_url.pm +6 -0
  40. package/project/.sdk/tm/perl/utility/prepare_headers.pm +21 -3
  41. package/project/.sdk/tm/perl/utility/prepare_query.pm +8 -0
  42. package/project/.sdk/tm/perl/utility/transform_request.pm +19 -7
  43. package/project/.sdk/tm/php/utility/MakeUrl.php +7 -0
  44. package/project/.sdk/tm/php/utility/PrepareHeaders.php +33 -4
  45. package/project/.sdk/tm/php/utility/PrepareQuery.php +11 -0
  46. package/project/.sdk/tm/php/utility/TransformRequest.php +28 -5
  47. package/project/.sdk/tm/py/pkg/utility/make_url.py +7 -0
  48. package/project/.sdk/tm/py/pkg/utility/prepare_headers.py +27 -6
  49. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +8 -0
  50. package/project/.sdk/tm/py/pkg/utility/transform_request.py +21 -5
  51. package/project/.sdk/tm/rb/utility/make_url.rb +7 -0
  52. package/project/.sdk/tm/rb/utility/prepare_headers.rb +21 -3
  53. package/project/.sdk/tm/rb/utility/prepare_query.rb +8 -0
  54. package/project/.sdk/tm/rb/utility/transform_request.rb +18 -5
  55. package/project/.sdk/tm/rust/utility/make_url.rs +10 -1
  56. package/project/.sdk/tm/rust/utility/prepare_headers.rs +43 -7
  57. package/project/.sdk/tm/rust/utility/prepare_query.rs +18 -1
  58. package/project/.sdk/tm/rust/utility/transform_request.rs +29 -7
  59. package/project/.sdk/tm/scala/utility/Make.scala +9 -0
  60. package/project/.sdk/tm/scala/utility/Prepare.scala +42 -3
  61. package/project/.sdk/tm/scala/utility/Transform.scala +30 -6
  62. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +7 -0
  63. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +28 -2
  64. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +23 -5
  65. package/project/.sdk/tm/ts/src/utility/MakeUrlUtility.ts +7 -0
  66. package/project/.sdk/tm/ts/src/utility/PrepareHeadersUtility.ts +16 -0
  67. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +3 -1
  68. package/project/.sdk/tm/ts/src/utility/TransformRequestUtility.ts +16 -3
  69. package/project/.sdk/tm/ts/test/definition-runner.ts +75 -13
  70. package/project/.sdk/tm/zig/core/utility.zig +80 -20
  71. package/project/sdkgen-package.json +1 -1
  72. package/src/helpers/definitionPlan.ts +49 -7
@@ -32,6 +32,7 @@ async function runDefinitionPoint(SDK, point) {
32
32
 
33
33
  const input = { ...point.select }
34
34
  for (const arg of point.args) input[arg.name] = arg.value
35
+ for (const h of point.headers || []) input[h.name] = h.value
35
36
  if (null != point.action) input.$action = point.action
36
37
 
37
38
  let result
@@ -53,7 +54,9 @@ async function runDefinitionPoint(SDK, point) {
53
54
  const arg = point.args.find((a) => a.wire === name)
54
55
  return null == arg ? '{' + name + '}' : encodeURIComponent(String(arg.value))
55
56
  })
56
- assert.equal(url.pathname, route, 'route')
57
+ // Read as the request was, so a backslash in the definition's path, as in
58
+ // GitLab's `Packages\(\)`, is the slash the URL parser makes it.
59
+ assert.equal(url.pathname, new URL(BASE + route).pathname, 'route')
57
60
 
58
61
  // Only what the definition declares, so never a path parameter again.
59
62
  const credentialQuery = (point.auth || []).flat()
@@ -70,6 +73,18 @@ async function runDefinitionPoint(SDK, point) {
70
73
  'credential not sent as the definition declares it: ' + JSON.stringify(point.auth))
71
74
  }
72
75
 
76
+ // A header parameter goes out as a header. The credential check above owns
77
+ // any header the security scheme names, and the SDK sets the content type
78
+ // from the body it sends.
79
+ const credentialHeaders = (point.auth || []).flat()
80
+ .filter((c) => 'header' === c.in).map((c) => c.name.toLowerCase())
81
+ for (const h of point.headers || []) {
82
+ const wire = h.wire.toLowerCase()
83
+ if (credentialHeaders.includes(wire) || 'content-type' === wire) continue
84
+ assert.equal(new Headers(init.headers).get(wire), String(h.value),
85
+ 'header parameter not sent as a header: ' + h.wire)
86
+ }
87
+
73
88
  if (null != error) {
74
89
  throw error
75
90
  }
@@ -88,7 +103,7 @@ async function runDefinitionPoint(SDK, point) {
88
103
  }
89
104
  }
90
105
  else if ('load' === point.op || 'create' === point.op || 'update' === point.op) {
91
- const record = recordOf(point.sample, point.idField)
106
+ const record = recordOf(point.sample, point.idField, point.entity)
92
107
  if (null != record) {
93
108
  assert.equal(result?.data?.()?.[point.idField], record[point.idField],
94
109
  'the entity does not hold the record the definition example returns')
@@ -128,33 +143,79 @@ function placed(cred, headers, url) {
128
143
  }
129
144
 
130
145
 
131
- // The records a list response holds: the body itself, or its one property
132
- // that is a non-empty list of objects. Anything else proves nothing.
146
+ // What an envelope may hold beside what it carries, compared without case,
147
+ // `_` or `-`: status, paging and the page's own metadata, each by its whole
148
+ // name, so a record's homepage or preview is data of its own.
149
+ const ENVELOPE_KEYS = new Set([
150
+ 'success', 'status', 'ok', 'message', 'code', 'error', 'errorcode', 'errormessage',
151
+ 'requestid', 'timestamp', 'took', 'version', 'apiversion', 'object', 'url',
152
+ 'count', 'total', 'totalcount', 'totalhits', 'totalitems', 'totalpages', 'totalresults',
153
+ 'totalrecords', 'totalrowcount', 'totalentries', 'itemcount', 'resultcount', 'rowcount',
154
+ 'page', 'pages', 'pagecount', 'pagenumber', 'pageindex', 'pagesize', 'perpage',
155
+ 'currentpage', 'lastpage', 'limit', 'offset', 'cursor', 'nextcursor', 'prevcursor',
156
+ 'previouscursor', 'next', 'nextpage', 'nexturl', 'nextlink', 'nextpagetoken', 'nexttoken',
157
+ 'pagetoken', 'continuationtoken', 'prev', 'previous', 'prevpage', 'previouspage',
158
+ 'prevurl', 'previousurl', 'prevlink', 'hasmore', 'hasnext', 'hasnextpage', 'hasprevious',
159
+ 'haspreviouspage', 'more', 'meta', 'metadata', 'pagination', 'paging', 'pageinfo', 'links',
160
+ ])
161
+
162
+
163
+ function envelopeKey(key) {
164
+ return ENVELOPE_KEYS.has(squash(key))
165
+ }
166
+
167
+
168
+ function ownData(sample) {
169
+ return Object.entries(sample).some(([key, value]) =>
170
+ null != value && 'object' !== typeof value && !envelopeKey(key))
171
+ }
172
+
173
+
174
+ function isRecord(value) {
175
+ return null != value && 'object' === typeof value && !Array.isArray(value)
176
+ }
177
+
178
+
179
+ // The records a list response holds: the body itself, or the one non-empty
180
+ // list of objects in a page. Anything else, such as a record that happens to
181
+ // hold a list, proves nothing.
133
182
  function recordsOf(sample) {
134
183
  if (Array.isArray(sample)) {
135
184
  return sample
136
185
  }
137
- if (null == sample || 'object' !== typeof sample) {
186
+ if (!isRecord(sample) || ownData(sample)) {
138
187
  return null
139
188
  }
140
189
  const lists = Object.values(sample).filter((v) => Array.isArray(v) && 0 < v.length &&
141
- v.every((x) => null != x && 'object' === typeof x && !Array.isArray(x)))
190
+ v.every(isRecord))
142
191
  return 1 === lists.length ? lists[0] : null
143
192
  }
144
193
 
145
194
 
146
- // The record a single-item response returns: the body, or its one property
147
- // holding an object with the identity field.
148
- function recordOf(sample, idField) {
149
- if (null == sample || 'object' !== typeof sample || Array.isArray(sample)) {
195
+ // The record a single-item response returns: the body, or the one object
196
+ // with the identity field that an envelope carries, under the entity's name
197
+ // or beside envelope keys alone. Otherwise the body is the record itself,
198
+ // such as GitHub's check suite preferences beside their repository.
199
+ function recordOf(sample, idField, entity) {
200
+ if (!isRecord(sample)) {
150
201
  return null
151
202
  }
152
203
  if (null != sample[idField]) {
153
204
  return sample
154
205
  }
155
- const inner = Object.values(sample).filter((v) => null != v && 'object' === typeof v &&
156
- !Array.isArray(v) && null != v[idField])
157
- return 1 === inner.length ? inner[0] : null
206
+ const keys = Object.keys(sample)
207
+ const inner = keys.filter((key) => isRecord(sample[key]) && null != sample[key][idField])
208
+ if (1 !== inner.length) {
209
+ return null
210
+ }
211
+ const named = null != entity && squash(inner[0]) === squash(entity) && !ownData(sample)
212
+ const alone = keys.every((key) => key === inner[0] || envelopeKey(key))
213
+ return named || alone ? sample[inner[0]] : null
214
+ }
215
+
216
+
217
+ function squash(name) {
218
+ return name.toLowerCase().replace(/[_-]/g, '')
158
219
  }
159
220
 
160
221
 
@@ -104,6 +104,14 @@ fun makeUrl(ctx: Context): String {
104
104
  joinParts.add(spec.suffix)
105
105
  var url = Struct.join(joinParts, "/", true)
106
106
 
107
+ // A route the definition ends with a slash keeps it: a server such as a
108
+ // Django REST one redirects or refuses the route without it.
109
+ val point = ctx.point
110
+ val orig = if (point == null) null else Struct.getprop(point, "orig", null)
111
+ if (orig is String && orig.endsWith("/") && spec.suffix.isEmpty() && !url.endsWith("/")) {
112
+ url += "/"
113
+ }
114
+
107
115
  val resmatch = linkedMapOf<String, Any?>()
108
116
 
109
117
  val params = spec.params
@@ -100,12 +100,32 @@ fun prepareHeaders(ctx: Context): MutableMap<String, Any?> {
100
100
  val options = ctx.client!!.optionsMap()
101
101
 
102
102
  val headers = Struct.getprop(options, "headers", null)
103
- if (headers == null) {
104
- return linkedMapOf()
103
+ val out: MutableMap<String, Any?> =
104
+ (if (headers == null) null else Helpers.toMapAny(Struct.clone(headers))) ?: linkedMapOf()
105
+
106
+ // A header parameter travels as a header, under the name the definition
107
+ // gives it, and only from this call's own arguments. It replaces a default
108
+ // of the same name, whatever its case.
109
+ val point = ctx.point
110
+ val hl = if (point == null) null else Struct.getpath(point, listOf("args", "header"))
111
+ if (hl is List<*>) {
112
+ for (hd in hl) {
113
+ val name = Struct.getprop(hd, "name")
114
+ if (name !is String || name.isEmpty()) {
115
+ continue
116
+ }
117
+ val orig = Struct.getprop(hd, "orig")
118
+ val wire = if (orig is String && orig.isNotEmpty()) orig else name
119
+ val v = Struct.getprop(ctx.reqmatch, name, null) ?: Struct.getprop(ctx.reqdata, name, null)
120
+ if (v != null) {
121
+ val key = wire.lowercase()
122
+ out.keys.removeAll { it.lowercase() == key }
123
+ out[key] = Struct.stringify(v)
124
+ }
125
+ }
105
126
  }
106
127
 
107
- val out = Helpers.toMapAny(Struct.clone(headers))
108
- return out ?: linkedMapOf()
128
+ return out
109
129
  }
110
130
 
111
131
  fun prepareMethod(ctx: Context): String? {
@@ -196,6 +216,16 @@ fun prepareQuery(ctx: Context): MutableMap<String, Any?> {
196
216
  }
197
217
  }
198
218
  }
219
+ // A header parameter travels in the headers, which prepareHeaders fills.
220
+ val hl = Struct.getpath(point, listOf("args", "header"))
221
+ if (hl is List<*>) {
222
+ for (hd in hl) {
223
+ val name = Struct.getprop(hd, "name")
224
+ if (name is String) {
225
+ params.add(name)
226
+ }
227
+ }
228
+ }
199
229
  }
200
230
 
201
231
  // A query parameter travels under the name the definition gives it, its
@@ -60,13 +60,23 @@ fun resultHeaders(ctx: Context): KOTLINPACKAGE.core.Result {
60
60
 
61
61
  // `$action` selects the point (see makePoint); it is never an API field, so
62
62
  // the body is a copy without it. The caller's map is left untouched.
63
- private fun stripAction(reqdata: Any?): Any? {
64
- if (reqdata !is Map<*, *> || !reqdata.containsKey("\$action")) {
63
+ private fun stripAction(reqdata: Any?): Any? = omitKeys(reqdata, listOf("\$action"))
64
+
65
+ // A header argument travels as a header, which prepareHeaders sends, so the
66
+ // body is built from the request data without it.
67
+ private fun headerArgNames(ctx: Context): List<String> {
68
+ val point = ctx.point ?: return emptyList()
69
+ val hl = Struct.getpath(point, listOf("args", "header")) as? List<*> ?: return emptyList()
70
+ return hl.mapNotNull { hd -> (Struct.getprop(hd, "name", null) as? String)?.takeIf { it.isNotEmpty() } }
71
+ }
72
+
73
+ private fun omitKeys(reqdata: Any?, names: List<String>): Any? {
74
+ if (reqdata !is Map<*, *> || names.none { reqdata.containsKey(it) }) {
65
75
  return reqdata
66
76
  }
67
77
  val body = linkedMapOf<String, Any?>()
68
78
  for ((k, v) in reqdata) {
69
- if ("\$action" != k) {
79
+ if (k.toString() !in names) {
70
80
  body[k.toString()] = v
71
81
  }
72
82
  }
@@ -78,14 +88,16 @@ fun transformRequest(ctx: Context): Any? {
78
88
  ctx.spec!!.step = "reqform"
79
89
  }
80
90
 
91
+ val reqdata = omitKeys(ctx.reqdata, headerArgNames(ctx))
92
+
81
93
  val transform = Helpers.toMapAny(Struct.getprop(ctx.point, "transform"))
82
- ?: return stripAction(ctx.reqdata)
94
+ ?: return stripAction(reqdata)
83
95
 
84
96
  val reqform = Struct.getprop(transform, "req", null)
85
- ?: return stripAction(ctx.reqdata)
97
+ ?: return stripAction(reqdata)
86
98
 
87
99
  val data = linkedMapOf<String, Any?>()
88
- data["reqdata"] = ctx.reqdata
100
+ data["reqdata"] = reqdata
89
101
 
90
102
  return stripAction(Struct.transform(data, reqform))
91
103
  }
@@ -18,6 +18,14 @@ local function make_url_util(ctx)
18
18
  local url = vs.join({ spec.base, spec.prefix, spec.path, spec.suffix }, "/", true)
19
19
  local resmatch = {}
20
20
 
21
+ -- A route the definition ends with a slash keeps it: a server such as a
22
+ -- Django REST one redirects or refuses the route without it.
23
+ local orig = ctx.point ~= nil and vs.getprop(ctx.point, "orig") or nil
24
+ if type(orig) == "string" and orig:sub(-1) == "/" and (spec.suffix == nil or spec.suffix == "")
25
+ and url:sub(-1) ~= "/" then
26
+ url = url .. "/"
27
+ end
28
+
21
29
  local param_items = vs.items(spec.params)
22
30
  if param_items ~= nil then
23
31
  for _, item in ipairs(param_items) do
@@ -6,15 +6,44 @@ local function prepare_headers_util(ctx)
6
6
  local options = ctx.client:options_map()
7
7
  local headers = vs.getprop(options, "headers")
8
8
 
9
- if headers == nil then
10
- return {}
9
+ local out = {}
10
+ if headers ~= nil then
11
+ local cloned = vs.clone(headers)
12
+ if type(cloned) == "table" then
13
+ out = cloned
14
+ end
11
15
  end
12
16
 
13
- local out = vs.clone(headers)
14
- if type(out) == "table" then
15
- return out
17
+ -- A header parameter travels as a header, under the name the definition
18
+ -- gives it, and only from this call's own arguments. It replaces a default
19
+ -- of the same name, whatever its case.
20
+ local hl = ctx.point ~= nil and vs.getpath(ctx.point, "args.header") or nil
21
+ if type(hl) == "table" then
22
+ for _, hd in ipairs(hl) do
23
+ local name = vs.getprop(hd, "name")
24
+ if type(name) == "string" and name ~= "" then
25
+ local orig = vs.getprop(hd, "orig")
26
+ if type(orig) ~= "string" or orig == "" then
27
+ orig = name
28
+ end
29
+ local val = vs.getprop(ctx.reqmatch or {}, name)
30
+ if val == nil then
31
+ val = vs.getprop(ctx.reqdata or {}, name)
32
+ end
33
+ if val ~= nil then
34
+ local wire = string.lower(orig)
35
+ for key in pairs(out) do
36
+ if type(key) == "string" and string.lower(key) == wire then
37
+ out[key] = nil
38
+ end
39
+ end
40
+ out[wire] = vs.stringify(val)
41
+ end
42
+ end
43
+ end
16
44
  end
17
- return {}
45
+
46
+ return out
18
47
  end
19
48
 
20
49
  return prepare_headers_util
@@ -34,6 +34,16 @@ local function prepare_query_util(ctx)
34
34
  end
35
35
  end
36
36
  end
37
+ -- A header parameter travels in the headers, which prepare_headers fills.
38
+ local hl = vs.getpath(point, "args.header")
39
+ if type(hl) == "table" then
40
+ for _, hd in ipairs(hl) do
41
+ local name = vs.getprop(hd, "name")
42
+ if type(name) == "string" then
43
+ table.insert(params, name)
44
+ end
45
+ end
46
+ end
37
47
  end
38
48
 
39
49
  -- A query parameter travels under the name the definition gives it, its
@@ -6,19 +6,48 @@ local helpers = require("core.helpers")
6
6
  -- `$action` selects the point (see make_point_util); it is never an API
7
7
  -- field, so the body is a copy without it. The caller's table is left
8
8
  -- untouched.
9
- local function strip_action(reqdata)
10
- if not vs.ismap(reqdata) or reqdata["$action"] == nil then
9
+ local function omit(reqdata, names)
10
+ if not vs.ismap(reqdata) then
11
+ return reqdata
12
+ end
13
+ local skip = {}
14
+ local found = false
15
+ for _, name in ipairs(names) do
16
+ skip[name] = true
17
+ found = found or reqdata[name] ~= nil
18
+ end
19
+ if not found then
11
20
  return reqdata
12
21
  end
13
22
  local body = {}
14
23
  for k, v in pairs(reqdata) do
15
- if k ~= "$action" then
24
+ if not skip[k] then
16
25
  body[k] = v
17
26
  end
18
27
  end
19
28
  return body
20
29
  end
21
30
 
31
+ local function strip_action(reqdata)
32
+ return omit(reqdata, { "$action" })
33
+ end
34
+
35
+ -- A header argument travels as a header, which prepare_headers_util sends, so
36
+ -- the body is built from the request data without it.
37
+ local function header_arg_names(point)
38
+ local names = {}
39
+ local hl = point ~= nil and vs.getpath(point, "args.header") or nil
40
+ if type(hl) == "table" then
41
+ for _, hd in ipairs(hl) do
42
+ local name = vs.getprop(hd, "name")
43
+ if type(name) == "string" and name ~= "" then
44
+ names[#names + 1] = name
45
+ end
46
+ end
47
+ end
48
+ return names
49
+ end
50
+
22
51
  local function transform_request_util(ctx)
23
52
  local spec = ctx.spec
24
53
  local point = ctx.point
@@ -27,18 +56,20 @@ local function transform_request_util(ctx)
27
56
  spec.step = "reqform"
28
57
  end
29
58
 
59
+ local data = omit(ctx.reqdata, header_arg_names(point))
60
+
30
61
  local transform = helpers.to_map(vs.getprop(point, "transform"))
31
62
  if transform == nil then
32
- return strip_action(ctx.reqdata)
63
+ return strip_action(data)
33
64
  end
34
65
 
35
66
  local reqform = vs.getprop(transform, "req")
36
67
  if reqform == nil then
37
- return strip_action(ctx.reqdata)
68
+ return strip_action(data)
38
69
  end
39
70
 
40
71
  local reqdata = vs.transform({
41
- reqdata = ctx.reqdata,
72
+ reqdata = data,
42
73
  }, reqform)
43
74
 
44
75
  return strip_action(reqdata)
@@ -355,9 +355,35 @@ let prepare_method_util (ctx : ctx) : string =
355
355
 
356
356
  let prepare_headers_util (ctx : ctx) : value =
357
357
  let options = client_options_map (cc ctx) in
358
- match getp options "headers" with
359
- | Noval -> empty_map ()
360
- | h -> (match clone h with Map _ as m -> m | _ -> empty_map ())
358
+ let out =
359
+ match getp options "headers" with
360
+ | Noval -> empty_map ()
361
+ | h -> (match clone h with Map _ as m -> m | _ -> empty_map ())
362
+ in
363
+ (* A header parameter travels as a header, under the name the definition
364
+ * gives it, and only from this call's own arguments. It replaces a default
365
+ * of the same name, whatever its case. *)
366
+ (match getp (getp ctx.c_point "args") "header" with
367
+ | List r ->
368
+ List.iter (fun hd ->
369
+ match getp hd "name" with
370
+ | Str name when name <> "" ->
371
+ let wire = match getp hd "orig" with Str o when o <> "" -> o | _ -> name in
372
+ let v = match getp ctx.c_reqmatch name with
373
+ | Noval | Null -> getp ctx.c_reqdata name
374
+ | v -> v
375
+ in
376
+ (match v with
377
+ | Noval | Null -> ()
378
+ | v ->
379
+ let key = String.lowercase_ascii wire in
380
+ List.iter (fun k ->
381
+ if String.lowercase_ascii k = key then ignore (delprop out (Str k)))
382
+ (keysof out);
383
+ setp out key (Str (stringify v)))
384
+ | _ -> ()) !r
385
+ | _ -> ());
386
+ out
361
387
 
362
388
  let param_util (ctx : ctx) (paramdef : value) : value =
363
389
  let point = ctx.c_point and spec = ctx.c_spec in
@@ -418,7 +444,13 @@ let prepare_query_util (ctx : ctx) : value =
418
444
  | List r -> List.map (fun pd -> getp pd "name") !r
419
445
  | _ -> []
420
446
  in
421
- let params = params @ arg_names in
447
+ (* A header parameter travels in the headers, which prepare_headers fills. *)
448
+ let header_names =
449
+ match getp (getp ctx.c_point "args") "header" with
450
+ | List r -> List.map (fun hd -> getp hd "name") !r
451
+ | _ -> []
452
+ in
453
+ let params = params @ arg_names @ header_names in
422
454
  let contains_param s = List.exists (fun v -> match v with Str x -> x = s | _ -> false) params in
423
455
  (* A query parameter travels under the name the definition gives it, its
424
456
  * orig, which the model may have renamed for the caller. *)
@@ -578,27 +610,39 @@ let prepare_auth_util = Sdk_prepare_auth.prepare_auth_util
578
610
 
579
611
  (* ----- transforms / result helpers ----- *)
580
612
 
581
- (* `$action` selects the point (see make_point_util); it is never an API
582
- field, so the body is a copy without it. The caller's map is left
583
- untouched. *)
584
- let strip_action (reqdata : value) : value =
613
+ let omit_keys (reqdata : value) (names : string list) : value =
585
614
  match reqdata with
586
- | Map _ when List.mem "$action" (keysof reqdata) ->
615
+ | Map _ when List.exists (fun n -> List.mem n (keysof reqdata)) names ->
587
616
  let body = empty_map () in
588
- List.iter (fun k -> if k <> "$action" then setp body k (getp reqdata k))
617
+ List.iter (fun k -> if not (List.mem k names) then setp body k (getp reqdata k))
589
618
  (keysof reqdata);
590
619
  body
591
620
  | _ -> reqdata
592
621
 
622
+ (* `$action` selects the point (see make_point_util); it is never an API
623
+ field, so the body is a copy without it. The caller's map is left
624
+ untouched. *)
625
+ let strip_action (reqdata : value) : value = omit_keys reqdata ["$action"]
626
+
627
+ (* A header argument travels as a header, which prepare_headers_util sends,
628
+ so the body is built from the request data without it. *)
629
+ let header_arg_names (point : value) : string list =
630
+ match getp (getp point "args") "header" with
631
+ | List r ->
632
+ List.filter_map (fun hd ->
633
+ match getp hd "name" with Str n when n <> "" -> Some n | _ -> None) !r
634
+ | _ -> []
635
+
593
636
  let transform_request_util (ctx : ctx) : value =
594
637
  (match ctx.c_spec with Some s -> s.sp_step <- "reqform" | None -> ());
638
+ let data = omit_keys ctx.c_reqdata (header_arg_names ctx.c_point) in
595
639
  strip_action
596
640
  (match to_map (getp ctx.c_point "transform") with
597
641
  | Map _ as tr ->
598
642
  (match getp tr "req" with
599
- | Noval -> ctx.c_reqdata
600
- | reqform -> transform (jo [("reqdata", ctx.c_reqdata)]) reqform)
601
- | _ -> ctx.c_reqdata)
643
+ | Noval -> data
644
+ | reqform -> transform (jo [("reqdata", data)]) reqform)
645
+ | _ -> data)
602
646
 
603
647
  let transform_response_util (ctx : ctx) : value =
604
648
  (match ctx.c_spec with Some s -> s.sp_step <- "resform" | None -> ());
@@ -804,6 +848,12 @@ let make_url_util (ctx : ctx) : (string * sdk_error option) =
804
848
  | _, None -> ("", Some (ctx_make_error ctx "url_no_result" "Expected context result property to be defined."))
805
849
  | Some spec, Some result ->
806
850
  let url = ref (join ~sep:(Str "/") ~url:true (ja [Str spec.sp_base; Str spec.sp_prefix; Str spec.sp_path; Str spec.sp_suffix])) in
851
+ (* A route the definition ends with a slash keeps it: a server such as a
852
+ * Django REST one redirects or refuses the route without it. *)
853
+ let ends_slash s = String.length s > 0 && '/' = s.[String.length s - 1] in
854
+ (match getp ctx.c_point "orig" with
855
+ | Str o when ends_slash o && spec.sp_suffix = "" && not (ends_slash !url) -> url := !url ^ "/"
856
+ | _ -> ());
807
857
  let resmatch = empty_map () in
808
858
  List.iter (fun key ->
809
859
  let v = getp spec.sp_params key in
@@ -29,6 +29,12 @@ $REGISTRY{make_url} = sub {
29
29
  [$spec->{base}, $spec->{prefix}, $spec->{path}, $spec->{suffix}], '/', 1);
30
30
  my $resmatch = {};
31
31
 
32
+ # A route the definition ends with a slash keeps it: a server such as a
33
+ # Django REST one redirects or refuses the route without it.
34
+ my $orig = $ctx->{point} ? ProjectNameHelpers::gp($ctx->{point}, 'orig') : undef;
35
+ $url .= '/' if defined $orig && !ref $orig && $orig =~ m{/\z}
36
+ && !(defined $spec->{suffix} && '' ne $spec->{suffix}) && $url !~ m{/\z};
37
+
32
38
  my $param_items = Voxgig::Struct::items($spec->{params});
33
39
  if ($param_items) {
34
40
  for my $item (@$param_items) {
@@ -19,9 +19,27 @@ $REGISTRY{prepare_headers} = sub {
19
19
  my ($ctx) = @_;
20
20
  my $options = $ctx->{client}->options_map;
21
21
  my $headers = ProjectNameHelpers::gp($options, 'headers');
22
- return {} unless ProjectNameHelpers::rb_truthy($headers);
23
- my $out = Voxgig::Struct::clone($headers);
24
- return Voxgig::Struct::ismap($out) ? $out : {};
22
+ my $out = ProjectNameHelpers::rb_truthy($headers) ? Voxgig::Struct::clone($headers) : {};
23
+ $out = {} unless Voxgig::Struct::ismap($out);
24
+ # A header parameter travels as a header, under the name the definition
25
+ # gives it, and only from this call's own arguments. It replaces a default
26
+ # of the same name, whatever its case.
27
+ my $hl = $ctx->{point} ? ProjectNameHelpers::gpath($ctx->{point}, 'args.header') : undef;
28
+ if (Voxgig::Struct::islist($hl)) {
29
+ for my $hd (@$hl) {
30
+ my $name = ProjectNameHelpers::gp($hd, 'name');
31
+ next unless defined $name && !ref $name && '' ne $name;
32
+ my $orig = ProjectNameHelpers::gp($hd, 'orig');
33
+ $orig = $name unless defined $orig && !ref $orig && '' ne $orig;
34
+ my $val = ProjectNameHelpers::gp($ctx->{reqmatch} || {}, $name);
35
+ $val = ProjectNameHelpers::gp($ctx->{reqdata} || {}, $name) unless defined $val;
36
+ next unless defined $val;
37
+ my $wire = lc $orig;
38
+ delete $out->{$_} for grep { lc $_ eq $wire } keys %$out;
39
+ $out->{$wire} = Voxgig::Struct::stringify($val);
40
+ }
41
+ }
42
+ return $out;
25
43
  };
26
44
 
27
45
  1;
@@ -32,6 +32,14 @@ $REGISTRY{prepare_query} = sub {
32
32
  push @$params, $name if defined $name && !ref $name;
33
33
  }
34
34
  }
35
+ # A header parameter travels in the headers, which prepare_headers fills.
36
+ my $hl = ProjectNameHelpers::gpath($point, 'args.header');
37
+ if (Voxgig::Struct::islist($hl)) {
38
+ for my $hd (@$hl) {
39
+ my $name = ProjectNameHelpers::gp($hd, 'name');
40
+ push @$params, $name if defined $name && !ref $name;
41
+ }
42
+ }
35
43
  }
36
44
  # A query parameter travels under the name the definition gives it, its
37
45
  # orig, which the model may have renamed for the caller.
@@ -17,24 +17,36 @@ our %REGISTRY;
17
17
 
18
18
  # `$action` selects the point (see make_point); it is never an API field, so
19
19
  # the body is a copy without it. The caller's hash is left untouched.
20
- my $strip_action = sub {
21
- my ($reqdata) = @_;
22
- return $reqdata unless 'HASH' eq ref $reqdata && exists $reqdata->{'$action'};
20
+ my $omit = sub {
21
+ my ($reqdata, @names) = @_;
22
+ return $reqdata unless 'HASH' eq ref $reqdata && grep { exists $reqdata->{$_} } @names;
23
23
  my %body = %$reqdata;
24
- delete $body{'$action'};
24
+ delete @body{@names};
25
25
  return \%body;
26
26
  };
27
27
 
28
+ my $strip_action = sub { $omit->($_[0], '$action') };
29
+
30
+ # A header argument travels as a header, which prepare_headers sends, so the
31
+ # body is built from the request data without it.
32
+ my $header_arg_names = sub {
33
+ my ($point) = @_;
34
+ my $hl = $point ? ProjectNameHelpers::gpath($point, 'args.header') : undef;
35
+ return () unless Voxgig::Struct::islist($hl);
36
+ return grep { defined $_ && !ref $_ && '' ne $_ } map { ProjectNameHelpers::gp($_, 'name') } @$hl;
37
+ };
38
+
28
39
  $REGISTRY{transform_request} = sub {
29
40
  my ($ctx) = @_;
30
41
  my $spec = $ctx->{spec};
31
42
  my $point = $ctx->{point};
32
43
  $spec->{step} = 'reqform' if $spec;
44
+ my $data = $omit->($ctx->{reqdata}, $header_arg_names->($point));
33
45
  my $transform = ProjectNameHelpers::to_map(ProjectNameHelpers::gp($point, 'transform'));
34
- return $strip_action->($ctx->{reqdata}) unless $transform;
46
+ return $strip_action->($data) unless $transform;
35
47
  my $reqform = ProjectNameHelpers::gp($transform, 'req');
36
- return $strip_action->($ctx->{reqdata}) unless ProjectNameHelpers::rb_truthy($reqform);
37
- return $strip_action->(Voxgig::Struct::transform({ 'reqdata' => $ctx->{reqdata} }, $reqform));
48
+ return $strip_action->($data) unless ProjectNameHelpers::rb_truthy($reqform);
49
+ return $strip_action->(Voxgig::Struct::transform({ 'reqdata' => $data }, $reqform));
38
50
  };
39
51
 
40
52
  1;
@@ -20,6 +20,13 @@ class ProjectNameMakeUrl
20
20
  $url = \Voxgig\Struct\Struct::join([$spec->base, $spec->prefix, $spec->path, $spec->suffix], '/', true);
21
21
  $resmatch = [];
22
22
 
23
+ // A route the definition ends with a slash keeps it: a server such as
24
+ // a Django REST one redirects or refuses the route without it.
25
+ $orig = $ctx->point ? \Voxgig\Struct\Struct::getprop($ctx->point, 'orig') : null;
26
+ if (is_string($orig) && str_ends_with($orig, '/') && '' === $spec->suffix && !str_ends_with($url, '/')) {
27
+ $url .= '/';
28
+ }
29
+
23
30
  $param_items = \Voxgig\Struct\Struct::items($spec->params);
24
31
  if ($param_items) {
25
32
  foreach ($param_items as $item) {