liquidsoap-prettier 1.8.2 → 1.8.3

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 (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. package/tests/liq/video.liq +918 -0
@@ -0,0 +1,196 @@
1
+ # Enable the external ffmpeg decoder.
2
+ # @category Liquidsoap
3
+ # @param ~file_extensions File extensions to decode. Should not be empty
4
+ # @param ~mimes Mime types to decode. Empty list means any type.
5
+ # @param ~binary Path to the `ffmpeg` binary.
6
+ def enable_external_ffmpeg_decoder(~binary="ffmpeg", ~mimes, ~file_extensions) =
7
+ decoder.add(
8
+ name="FFMPEG",
9
+ description="Decode files using the ffmpeg decoder binary",
10
+ mimes=mimes,
11
+ file_extensions=file_extensions,
12
+ fun (~rlog, ~maxtime:_, fname) ->
13
+ begin
14
+ # File is cleaned up as part of the request workflow.
15
+ outfile = file.temp(cleanup=false, "ffmpeg", ".wav")
16
+ try
17
+ let {status = {code}} =
18
+ process.run(
19
+ "#{binary} -i #{process.quote(fname)} #{process.quote(outfile)}"
20
+ )
21
+ code == 0 ? outfile : null
22
+ catch err do
23
+ file.remove(outfile)
24
+ rlog(
25
+ "Error while decoding #{fname} using ffmpeg: #{err}"
26
+ )
27
+ null
28
+ end
29
+ end
30
+ )
31
+ end
32
+
33
+ # Enable the external openmpt123 decoder
34
+ # @category Liquidsoap
35
+ # @param ~file_extensions File extensions to decode.
36
+ # @param ~mimes Mime types to decode. Empty list means any type.
37
+ # @param ~options Extra options.
38
+ # @param ~binary Path to the `ffmpeg` binary.
39
+ def enable_external_openmpt123_decoder(
40
+ ~binary="openmpt123",
41
+ ~mimes=[
42
+ "audio/it",
43
+ "audio/xm",
44
+ "audio/s3m",
45
+ "audio/x-mod",
46
+ "audio/mod",
47
+ "audio/module-xm",
48
+ "audio/x-mod",
49
+ "application/playerpro",
50
+ "audio/x-s3m",
51
+ "application/soundapp",
52
+ "audio/med",
53
+ "audio/x-xm"
54
+ ],
55
+ ~file_extensions=[
56
+ "xm",
57
+ "mtm",
58
+ "amf",
59
+ "stm",
60
+ "ult",
61
+ "wow",
62
+ "dmf",
63
+ "it",
64
+ "s3m",
65
+ "far",
66
+ "mod",
67
+ "mt2",
68
+ "okt",
69
+ "med",
70
+ "669"
71
+ ],
72
+ ~options=""
73
+ ) =
74
+ decoder.add(
75
+ name="OPENMPT123",
76
+ description="Decode files using the openmpt123 decoder binary",
77
+ mimes=mimes,
78
+ file_extensions=file_extensions,
79
+ fun (~rlog, ~maxtime:_, infile) ->
80
+ begin
81
+ ret =
82
+ process.read.lines(
83
+ "#{binary} --info #{process.quote(infile)} 2>&1"
84
+ )
85
+ def get_meta(l, s) =
86
+ ret = string.extract(pattern="^(\\w+).+:\\s(.+)$", s)
87
+ if
88
+ list.length(ret) > 2
89
+ then
90
+ label = ret[1]
91
+ val = ret[2]
92
+ label = "openmpt:#{string.case(lower=true, label)}"
93
+ ["#{string.quote(label)}=#{string.quote(val)}", ...l]
94
+ else
95
+ l
96
+ end
97
+ end
98
+ meta = list.fold(get_meta, [], ret)
99
+
100
+ prefix =
101
+ if
102
+ meta == []
103
+ then
104
+ ""
105
+ else
106
+ "annotate:#{string.concat(separator=',', meta)}:"
107
+ end
108
+
109
+ # File is cleaned up as part of the request workflow.
110
+ outfile = file.temp(cleanup=false, "openmpt", ".wav")
111
+ try
112
+ let {status = {code}} =
113
+ process.run(
114
+ "#{binary} --assume-terminal --quiet --force #{options} --output #{
115
+ process.quote(outfile)
116
+ } #{process.quote(infile)}"
117
+ )
118
+ code == 0 ? "#{prefix}#{outfile}" : null
119
+ catch err do
120
+ file.remove(outfile)
121
+ rlog(
122
+ "Error while decoding #{infile} using ffmpeg: #{err}"
123
+ )
124
+ null
125
+ end
126
+ end
127
+ )
128
+ end
129
+
130
+ # Standard function for displaying metadata.
131
+ # Shows artist and title, using "Unknown" when a field is empty.
132
+ # @param m Metadata packet to be displayed.
133
+ # @category String
134
+ def string_of_metadata(m) =
135
+ artist = m["artist"]
136
+ title = m["title"]
137
+ artist = if "" == artist then "Unknown" else artist end
138
+ title = if "" == title then "Unknown" else title end
139
+ "#{artist} -- #{title}"
140
+ end
141
+
142
+ # Use X On Screen Display to display metadata info.
143
+ # @flag extra
144
+ # @param ~color Color of the text.
145
+ # @param ~position Position of the text (top|middle|bottom).
146
+ # @param ~font Font used (xfontsel is your friend...)
147
+ # @param ~display Function used to display a metadata packet.
148
+ # @category Source / Track processing
149
+ def osd_metadata(
150
+ ~color="green",
151
+ ~position="top",
152
+ ~font="-*-courier-*-r-*-*-*-240-*-*-*-*-*-*",
153
+ ~display=string_of_metadata,
154
+ s
155
+ ) =
156
+ osd =
157
+ 'osd_cat -p #{position} --font #{process.quote(font)}' ^
158
+ ' --color #{color}'
159
+
160
+ def feedback(m) =
161
+ ignore(
162
+ process.run(
163
+ "echo #{process.quote(display(m))} | #{osd} &"
164
+ )
165
+ )
166
+ end
167
+
168
+ s.on_metadata(synchronous=false, feedback)
169
+ end
170
+
171
+ # Use notify to display metadata info.
172
+ # @flag extra
173
+ # @param ~urgency Urgency (low|normal|critical).
174
+ # @param ~icon Icon filename or stock icon to display.
175
+ # @param ~timeout Timeout in seconds.
176
+ # @param ~display Function used to display a metadata packet.
177
+ # @param ~title Title of the notification message.
178
+ # @category Source / Track processing
179
+ def notify_metadata(
180
+ ~urgency="low",
181
+ ~icon="stock_smiley-22",
182
+ ~timeout=3.,
183
+ ~display=string_of_metadata,
184
+ ~title="Liquidsoap: new track",
185
+ s
186
+ ) =
187
+ time = int_of_float(timeout * 1000.)
188
+ send =
189
+ 'notify-send -i #{icon} -u #{urgency}' ^
190
+ ' -t #{time} #{process.quote(title)} '
191
+
192
+ s.on_metadata(
193
+ synchronous=false,
194
+ fun (m) -> ignore(process.run(send ^ process.quote(display(m))))
195
+ )
196
+ end
@@ -0,0 +1,260 @@
1
+ # Plot the first crossfade transition. Used for visualizing and testing
2
+ # crossfade transitions.
3
+ # @category Source / Track processing
4
+ # @flag extra
5
+ def cross.plot(~png=null, ~dir=null, ~font=null, ~font_size=10, s) =
6
+ dir =
7
+ if
8
+ null.defined(dir)
9
+ then
10
+ null.get(dir)
11
+ else
12
+ dir = file.temp_dir("plot")
13
+ on_cleanup({file.rmdir(dir)})
14
+ dir
15
+ end
16
+
17
+ old_txt = path.concat(dir, "old.txt")
18
+ new_txt = path.concat(dir, "new.txt")
19
+
20
+ def gnuplot_cmd(filename) =
21
+ term =
22
+ if
23
+ null.defined(font)
24
+ then
25
+ 'pngcairo font "#{null.get(font)},#{font_size}"'
26
+ else
27
+ "png"
28
+ end
29
+ 'set term #{term}; set output "#{(filename : string)}"; plot "#{new_txt}" \
30
+ using 1:2 with lines title "new track", "#{old_txt}" using 1:2 with lines \
31
+ title "old track"'
32
+ end
33
+
34
+ def store_rms(~id, s) =
35
+ s = rms(duration=settings.frame.duration(), s)
36
+ t0 = ref(null)
37
+
38
+ s.on_frame(
39
+ synchronous=true,
40
+ before=false,
41
+ {
42
+ let t0 =
43
+ if
44
+ null.defined(t0())
45
+ then
46
+ null.get(t0())
47
+ else
48
+ t0 := source.time(s)
49
+ null.get(t0())
50
+ end
51
+ let v = float.truncate(digits=4, mode="floor", s.rms())
52
+ let p = float.truncate(digits=4, mode="floor", source.time(s) - t0)
53
+ fname = id == "old" ? old_txt : new_txt
54
+ file.write(append=true, data="#{p}\t#{v}\n", fname)
55
+ }
56
+ )
57
+
58
+ s
59
+ end
60
+
61
+ plotted = ref(false)
62
+
63
+ def transition(old, new) =
64
+ old = store_rms(id="old", fade.out(old.source))
65
+ new = store_rms(id="new", fade.in(new.source))
66
+
67
+ s = blank(duration=0.1)
68
+ s.on_frame(
69
+ synchronous=true,
70
+ {
71
+ if
72
+ null.defined(png) and not plotted()
73
+ then
74
+ ignore(
75
+ process.run(
76
+ "gnuplot -e #{process.quote(gnuplot_cmd(null.get(png)))}"
77
+ )
78
+ )
79
+ end
80
+ plotted := true
81
+ }
82
+ )
83
+
84
+ sequence(single_track=false, [add(normalize=false, [new, old]), once(s)])
85
+ end
86
+
87
+ cross(transition, s)
88
+ end
89
+
90
+ # Plot two sine tracks with given autocue params.
91
+ # Used for visualizing and testing autocue crossfade transitions.
92
+ # @category Source / Track processing
93
+ # @flag extra
94
+ def autocue.plot(
95
+ ~png=null,
96
+ ~dir=null,
97
+ ~font=null,
98
+ ~font_size=10,
99
+ ~old_autocue,
100
+ ~new_autocue,
101
+ ~sync="auto",
102
+ ~on_stop
103
+ ) =
104
+ dir =
105
+ if
106
+ null.defined(dir)
107
+ then
108
+ null.get(dir)
109
+ else
110
+ dir = file.temp_dir("plot")
111
+ on_cleanup({file.rmdir(dir)})
112
+ dir
113
+ end
114
+
115
+ old_txt = path.concat(dir, "old.txt")
116
+ new_txt = path.concat(dir, "new.txt")
117
+
118
+ def gnuplot_cmd(filename) =
119
+ term =
120
+ if
121
+ null.defined(font)
122
+ then
123
+ 'pngcairo font "#{null.get(font)},#{font_size}"'
124
+ else
125
+ "png"
126
+ end
127
+ 'set term #{term}; set output "#{(filename : string)}"; plot "#{new_txt}" \
128
+ using 1:2 with lines title "new track", "#{old_txt}" using 1:2 with lines \
129
+ title "old track"'
130
+ end
131
+
132
+ def autocue_annotate(a, uri) =
133
+ meta = autocue.metadata(implementation="autocue.plot", a)
134
+ meta =
135
+ list.map(fun ((label, value)) -> "#{label}=#{string.quote(value)}", meta)
136
+ meta = string.concat(separator=",", meta)
137
+ "annotate:#{meta}:#{uri}"
138
+ end
139
+
140
+ old_uri =
141
+ process.uri(
142
+ extname="wav",
143
+ "ffmpeg -f lavfi -i \"sine=frequency=1000:duration=10\" -y $(output) < \
144
+ /dev/null"
145
+ )
146
+ old_uri = autocue_annotate(old_autocue, old_uri)
147
+ old_source = request.once(request.create(old_uri))
148
+
149
+ new_uri =
150
+ process.uri(
151
+ extname="wav",
152
+ "ffmpeg -f lavfi -i \"sine=frequency=500:duration=10\" -y $(output) < \
153
+ /dev/null"
154
+ )
155
+ new_uri = autocue_annotate(new_autocue, new_uri)
156
+ new_source = request.once(request.create(new_uri))
157
+
158
+ s = sequence([old_source, new_source])
159
+
160
+ def store_rms(~fname, s) =
161
+ s = rms(duration=settings.frame.duration(), s)
162
+ t0 = ref(null)
163
+
164
+ s.on_frame(
165
+ synchronous=true,
166
+ before=false,
167
+ {
168
+ let t0 =
169
+ if
170
+ null.defined(t0())
171
+ then
172
+ null.get(t0())
173
+ else
174
+ t0 := source.time(s)
175
+ null.get(t0())
176
+ end
177
+ let t = float.truncate(digits=4, mode="floor", source.time(s) - t0)
178
+ let v = float.truncate(digits=4, mode="floor", s.rms())
179
+ file.write(append=true, data="#{t}\t#{v}\n", fname)
180
+ }
181
+ )
182
+
183
+ s
184
+ end
185
+
186
+ is_first_ending = ref(true)
187
+ is_first_starting = ref(true)
188
+
189
+ def transition(old, new) =
190
+ list.iter(
191
+ fun (x) ->
192
+ log(
193
+ level=4,
194
+ "Before: #{x}"
195
+ ),
196
+ old.metadata
197
+ )
198
+
199
+ list.iter(
200
+ fun (x) ->
201
+ log(
202
+ level=4,
203
+ "After : #{x}"
204
+ ),
205
+ new.metadata
206
+ )
207
+
208
+ def ending_map(s) =
209
+ if
210
+ is_first_ending()
211
+ then
212
+ is_first_ending := false
213
+ store_rms(fname=old_txt, s)
214
+ else
215
+ s
216
+ end
217
+ end
218
+
219
+ def starting_map(s) =
220
+ if
221
+ is_first_starting()
222
+ then
223
+ is_first_starting := false
224
+ store_rms(fname=new_txt, s)
225
+ else
226
+ s
227
+ end
228
+ end
229
+
230
+ cross.simple(
231
+ initial_fade_in_metadata=new.metadata,
232
+ initial_fade_out_metadata=old.metadata,
233
+ ending_map=ending_map,
234
+ starting_map=starting_map,
235
+ old.source,
236
+ new.source
237
+ )
238
+ end
239
+
240
+ s = cross(transition, s)
241
+
242
+ clock.assign_new(sync=sync, [s])
243
+
244
+ def on_stop() =
245
+ if
246
+ null.defined(png)
247
+ then
248
+ ignore(
249
+ process.run(
250
+ "gnuplot -e #{process.quote(gnuplot_cmd(null.get(png)))}"
251
+ )
252
+ )
253
+ end
254
+ on_stop()
255
+ clock(s.clock).stop()
256
+ end
257
+
258
+ o = output.dummy(fallible=true, s)
259
+ o.on_stop(synchronous=true, on_stop)
260
+ end
@@ -0,0 +1,66 @@
1
+ # Keep a record of played files. This is primarily useful to know when a song
2
+ # was last played and avoid repetitions.
3
+ # @flag extra
4
+ # @param ~duration Duration (in seconds) after which songs are forgotten. By default, songs are not forgotten which means that the playlog will contain all the songs ever played.
5
+ # @param ~hash Function to extract an identifier from the metadata. By default, the filename is used but we could return the artist to know when a song from a given artist was last played for instance.
6
+ # @param ~persistency Set a file name where the values are stored and loaded in case the script is restarted.
7
+ # @category Source / Track processing
8
+ # @method add Record that file with given metadata has been played.
9
+ # @method last How long ago a file was played (in seconds), `infinity` is returned if the song has never been played.
10
+ def playlog(
11
+ ~duration=infinity,
12
+ ~persistency=null,
13
+ ~hash=fun (m) -> m["filename"]
14
+ ) =
15
+ l = ref([])
16
+
17
+ # Load from persistency file
18
+ if
19
+ null.defined(persistency)
20
+ then
21
+ if
22
+ file.exists(null.get(persistency))
23
+ then
24
+ let json.parse (parsed : [(string * float)]?) =
25
+ file.contents(null.get(persistency))
26
+
27
+ if null.defined(parsed) then l := null.get(parsed) end
28
+ end
29
+ end
30
+
31
+ # Save into persistency file
32
+ def save() =
33
+ if
34
+ null.defined(persistency)
35
+ then
36
+ data = json.stringify(l())
37
+ file.write(data=data, null.get(persistency))
38
+ end
39
+ end
40
+
41
+ # Remove too old elements
42
+ def prune() =
43
+ if
44
+ duration != infinity
45
+ then
46
+ t = time()
47
+ l := list.assoc.filter(fun (_, tf) -> t - tf <= duration, l())
48
+ end
49
+ end
50
+
51
+ # Add a new entry
52
+ def add(m) =
53
+ prune()
54
+ f = hash(m)
55
+ l := (f, time())::l()
56
+ save()
57
+ end
58
+
59
+ # Last time this entry was played
60
+ def last(m) =
61
+ f = hash(m)
62
+ time() - list.assoc(default=0. - infinity, f, l())
63
+ end
64
+
65
+ {add = add, last = last}
66
+ end
@@ -0,0 +1,160 @@
1
+ # Harbor middleware to add CORS headers
2
+ # @category Internet
3
+ # @flag extra
4
+ # @param ~origin Configures the Access-Control-Allow-Origin CORS header
5
+ # @param ~origin_callback Origin callback for advanced uses. If passed, overrides `origin` argument. Takes the request as input and returns the allowed origin. Return `null` to skip all CORS headers.
6
+ # @param ~methods Configures the Access-Control-Allow-Methods CORS header.
7
+ # @param ~allowed_headers Configures the Access-Control-Allow-Headers CORS header. If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header.
8
+ # @param ~exposed_headers Configures the Access-Control-Expose-Headers CORS header. If not specified, no custom headers are exposed.
9
+ # @param ~credentials Configures the Access-Control-Allow-Credentials CORS header. Set to true to pass the header, otherwise it is omitted.
10
+ # @param ~max_age Configures the Access-Control-Max-Age CORS header. Set to an integer to pass the header, otherwise it is omitted.
11
+ # @param ~preflight_continue Pass the CORS preflight response to the nexnhandler.
12
+ # @param ~options_status_code Provides a status code to use for successful OPTIONS requests, since some legacy browsers (IE11, various SmartTVs) choke on 204.
13
+ def harbor.http.middleware.cors(
14
+ ~origin=null("*"),
15
+ ~origin_callback=null,
16
+ ~methods=["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
17
+ ~allowed_headers=null,
18
+ ~exposed_headers=[],
19
+ ~credentials=false,
20
+ ~max_age=null,
21
+ ~preflight_continue=false,
22
+ ~options_status_code=204
23
+ ) =
24
+ fun (req, res, next) ->
25
+ begin
26
+ # This is for typing purposes
27
+ res = if false then http.response() else res end
28
+ if
29
+ false
30
+ then
31
+ harbor.http.register(
32
+ "/foo",
33
+ fun (r, _) -> ignore(if false then r else req end)
34
+ )
35
+ end
36
+
37
+ vary = ref([])
38
+
39
+ def add_vary() =
40
+ if
41
+ vary() != []
42
+ then
43
+ res.header("Vary", string.concat(separator=",", vary()))
44
+ end
45
+ end
46
+
47
+ def vary(v) =
48
+ vary := v::vary()
49
+ end
50
+
51
+ def add_origin(origin) =
52
+ res.header("Access-Control-Allow-Origin", origin)
53
+ if origin != "*" then vary("Origin") end
54
+ end
55
+
56
+ def add_methods() =
57
+ if
58
+ methods != []
59
+ then
60
+ res.header(
61
+ "Access-Control-Allow-Methods",
62
+ string.concat(separator=",", methods)
63
+ )
64
+ end
65
+ end
66
+
67
+ def add_credentials() =
68
+ if
69
+ credentials
70
+ then
71
+ res.header("Access-Control-Allow-Credentials", "true")
72
+ end
73
+ end
74
+
75
+ def add_allowed_headers() =
76
+ allowed_headers =
77
+ if
78
+ null.defined(allowed_headers)
79
+ then
80
+ string.concat(separator=",", null.get(allowed_headers))
81
+ elsif
82
+ list.assoc.mem("access-control-request-headers", req.headers)
83
+ then
84
+ req.headers["access-control-request-headers"]
85
+ else
86
+ null
87
+ end
88
+
89
+ if
90
+ null.defined(allowed_headers)
91
+ then
92
+ res.header("Access-Control-Allow-Headers", null.get(allowed_headers))
93
+ vary("Access-Control-Request-Headers")
94
+ end
95
+ end
96
+
97
+ def add_exposed_headers() =
98
+ if
99
+ exposed_headers != []
100
+ then
101
+ res.header(
102
+ "Access-Control-Expose-Headers",
103
+ string.concat(separator=",", exposed_headers)
104
+ )
105
+ end
106
+ end
107
+
108
+ def add_max_age() =
109
+ if
110
+ null.defined(max_age)
111
+ then
112
+ res.header("Access-Control-Max-Age", "#{(null.get(max_age) : int)}")
113
+ end
114
+ end
115
+
116
+ origin =
117
+ if
118
+ null.defined(origin_callback)
119
+ then
120
+ fn = null.get(origin_callback)
121
+ fn(req)
122
+ else
123
+ getter.get(origin)
124
+ end
125
+
126
+ if
127
+ not null.defined(origin)
128
+ then
129
+ next(req, res)
130
+ else
131
+ add_origin(null.get(origin))
132
+ if
133
+ req.method == "OPTIONS"
134
+ then
135
+ add_credentials()
136
+ add_methods()
137
+ add_allowed_headers()
138
+ add_max_age()
139
+ add_exposed_headers()
140
+ add_vary()
141
+ if
142
+ preflight_continue
143
+ then
144
+ next(req, res)
145
+ else
146
+ res.status_code(options_status_code)
147
+ res.header("Content-length", "0")
148
+ end
149
+ else
150
+ add_credentials()
151
+ add_allowed_headers()
152
+ add_vary()
153
+ next(req, res)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ # This is for typing purposes
160
+ if false then harbor.http.middleware.register(harbor.http.middleware.cors()) end