langsmoke/ref_apl

2128 lines
84 KiB
Plaintext
Raw Normal View History

2023-11-12 13:49:57 +00:00
lam←{
⎕IO⎕ML←1 1
∆d←⍪('true' ('Lam' (,'t') ('Lam' (,'f') ('Var' (,'t')))))
∆d,←('false' ('Lam' (,'t') ('Lam' (,'f') ('Var' (,'f')))))
←∆d⋄∆t←⋄∆hi←{∆t,←⍺⍵}⋄∆hr←{∆t∘←((∆t↑[2]⍨¯1∘+),∆t↓[2]⍨⊢)⍵⍳⍨,1↑∆t}⋄err←{⍵⎕SIGNAL 8}
hash←{{1e10|+31×⍵}/128+1(220⌶)⍵}⋄sd←'₀₁₂₃₄₅₆₇₈₉'⋄l←'()λ.='⋄ad←{,sd[,1+10⊥⍣¯1⊢⍵]}
str←{'Lam'≡⊃⍵:∊'(λ'(2⊃⍵)'. '(∇3⊃⍵)')'⋄'Var'≡⊃⍵:2⊃⍵⋄'App'≡⊃⍵:∊'('(∇2⊃⍵)' '(∇3⊃⍵)')'}
lx←{0=≢⍵:⍬⋄(⊃⍵)∊l:(⊂0,⊃⍵),∇1↓⍵⋄3≠(⎕UCS 10 32)⍳⊃⍵:∇1↓⍵⋄'#'=⊃⍵:∇⍵↓⍨⍵⎕UCS 10
×k←⊥⍨⌽⍵∊sd,⎕A,⎕C⎕A:(⊂1,k↑⍵),∇k↓⍵⋄err'eltoken'⍵}
pr←{L←0'λ'⋄P←0'('⋄E←0'='⋄C←0')'⋄D←0'.'
at←{P≡⊃⍵:{lx t←tr(1↓⍵)⋄C≢⊃lx:err'eparen'⋄(1↓lx) t}⍵
1≡⊃⊃⍵:(1↓⍵)('Var'(1↓⊃⍵))⋄L≡⊃⍵:ab 1↓⍵⋄err'etoken'}
ab←{i←⊥⍨⌽1=⊃¨⍵⋄i<1:err'elambda'⋄D≢⍵⊃⍨i+1:err'edot'
lx tv←tr(1+i)↓⍵⋄nm←1↓¨i↑⍵⋄lx(nm{0=≢⍺:⍵⋄(¯1↓)∇'Lam'(⊃⌽⍺)⍵}tv)}
tr←{L≡⊃⍵:ab 1↓⍵⋄↑{(L≢⊃)∧(1≢⊃⊃)∧(P≢⊃):⍵⋄lx t←at ⋄lx ∇'App'⍵ t}/at ⍵}
bi←{k←⊃⍵⋄lx v←tr 2↓⍵⋄0≠≢lx:err'estray'⋄_←∆hr k⋄_←∆hi k v⋄⍬}
E≡⊃1↓⍵:bi ⍵⋄⊃⌽tr ⍵}
a←⍪''0⋄ac←{∆i←{i←(,1↑a)⊂⍵⋄_←{i>⊃⌽a:a,←⍵ 0⋄a[2;i]+←⋄0}⍵⋄⍵ad,a[2;i]}
{'Var'≡⊃⍵:'Var' (0 ∆i ⊃⌽⍵)⋄'App'≡⊃⍵:'App' (∇2⊃⍵) (∇3⊃⍵)
'Lam'≡⊃⍵:(⊂'Lam'),((∇3⊃⍵) ,⍨⍥⊂ (1 ∆i 2⊃⍵))}⍵}
de←{lk←{(⊂⍵)∊⍺:'Var'⍵⋄i←(⊂⍵)⍳⍨,1↑∆t⋄i>⊃⌽∆t:'Var'⍵⋄⊃∆t[2;i]}
{⍺←⊂''⋄'Var'≡⊃⍵:( lk ⊃⌽⍵)⋄'App'≡⊃⍵:'App' (∇2⊃⍵) (∇3⊃⍵)
'Lam'≡⊃⍵:(2↑⍵),⊂(,⊂2⊃⍵)∇3⊃⍵}⍵}
br←{'Lam'≡⊃⍵:(2↑⍵),⊂∇3⊃⍵⋄'Var'≡⊃⍵:⍵⋄'App'≢⊃⍵:err'eint'
an bn←∇¨1↓⍵⋄'Lam'≢⊃an:⍵⋄an bn←1↓ac 'App' an bn⋄av←2⊃an
{v←'Var'≡⊃⍵⋄v∧av≡2⊃⍵:bn⋄v:⍵⋄v←'Lam'≡⊃⍵⋄v∧av≡2⊃⍵:⍵⋄v:(2↑⍵),⊂∇3⊃⍵
'App'≡⊃⍵:'App' (∇2⊃⍵) (∇3⊃⍵)}3⊃an}
rd←{h←⍬⋄i←{⍵∊h:1⋄h,←⍵⋄0}⋄in←de ⍵⋄r←br⍣{(i hash )∨⍺≡⍵}in⋄(in≡r)(hash r)∊¯1↓h:err'einf'⋄r}
⍬≢ast←pr lx ⍵:str rd ast
}
⍝ apl-misc-math - Copyright (C) Kamila Szewczyk, 2022.
⍝ Redistributed under the terms of the AGPLv3 license.
⍝ Load using: ⎕fix'file:///.../apl-misc-math/mm.apl'⋄mm.setup
⍝ Special thanks to Adám Brudzewsky.
:Namespace mm
⍝ Default settings. The library works optimally with
⍝ higher precision arithmetic.
##.(⎕FR⎕PP)←1287 34
⍝ Alter to change the precision of operations.
⍝ Note: A value too small will carry more error due to
⍝ floating point inaccurancy.
epsilon←0.0000001
int_prec←0.0001
⍝ Braces were supposed to make the result shy, but apparently they don't.
∇ {r}←setup
(_tanh_sinh_pf _tanh_sinh_m2)←↓(○.5)×5 6∘.○int_prec×÷int_prec
_tanh_sinh_m2×←int_prec
(_tanh_xk _tanh_wkd)←↓7 6∘.○_tanh_sinh_pf
_tanh_sinh_m2÷←×⍨_tanh_wkd
_erf_c←2÷(○1)*.5
euler_gamma←(+/∘÷∘⍳-⍟) lim_inf 1 ⍝ Alternatively: -digamma 1
'ok'
⍝ d/dx |x=⍵
derv←{epsilon÷⍨-/⍺⍺¨⍵+epsilon 0}
⍝ d^n/dx^n |x=⍵
nderv←{⍵⍵=1: D ⍵ ⋄ (( D) ∇∇ (⍵⍵-1)) ⍵}
⍝ The secant root-finding method. ⍵ is starting x1,x2
secant←{
f←⋄⊃⌽{
dy←-/y1 y2←f¨x1 x2←⍵
x2,x1-y1×dy÷⍨-/⍵
}⍣{epsilon>|-/}⍵
}
⍝ Trim insignificant real/imaginary parts.
ztrim←{¯9 ¯11+.○(⊢×epsilon<|)9 11∘.○⍵}
⍝ Durand-Kerner method for finding complex polynomial roots.
⍝ 0.4J0.9 was chosen arbitrarily as a starting point. It is
⍝ neither a real number nor a de Moivre number.
durand_kerner←{
f←⊥∘((⊢÷⊃)⍵)⋄g←{⍵⍪⍉⍪f¨⍵}
ztrim¨,1↑{
v←,1↑⍵⋄g{-⍵÷×/0~⍨⍺-v}⌿⍵
}⍣⍺ g 0.4J0.9*⎕io-⍨1-⍨≢⍵
}
⍝ The Faddeev-LeVerrier algorithm for finding the characteristic
⍝ polynomial of a square matrix.
faddeev_leverrier←{
⎕io←0⋄(≠/⍴⍵)2≠≢⍵:⍬⋄n←≢⍵
M0←⍵⋄I←n n1↑⍨1+n⋄⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵⋄(cp,c)(X+I×c)
} n
}
⍝ An extension to the Faddeev-LeVerrier implementation above that
⍝ also keeps track of the matrix used to compute the inverse.
⍝ The inverse can be obtained using inv cpoly←... and inv×-÷⊃⌽cpoly
faddeev_leverrier_ex←{
⎕io←0⋄(≠/⍴⍵)2≠≢⍵:⍬⋄n←≢⍵⋄inv←⍬
M0←⍵⋄I←n n1↑⍨1+n⋄cpoly←⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵
MC←X+I×c
_←{⍵=n-1:inv∘←MC⋄0}⍵
(cp,c)MC
} n
inv cpoly
}
⍝ Eigenvector computation.
eigenvec←{
⎕io←0⋄(≠/⍴⍵)2≠≢⍵:⍬
n←≢⍵⋄I←n n1↑⍨1+n⋄s←⍵-×I
q←1,⍨1↑⍨1-⍨⊃⌽s⋄ztrim¨1,⍨∊⌹⍨∘-/q⊂1↓s
}
⍝ A range function from dfns.
range←{↑+/⍵{⍵×{⍵-⎕IO}1+0⌈⌊(-)÷⍵+⍵=0}\1 ¯1×-\2↑,+×⍵-}
⍝ Simpson integration. Assumes bounds <⍵.
simpson←{
h←(⍵-)÷S←÷int_prec
(h÷3)×+/(+⍥⍺⍺ ⍵),((⍺⍺⊣+h×⊢)×2×1+2|⊢⍤0)S
}
⍝ Trapezoidal rule.
trapz←{
=⍵:0
sgn←¯1*>⍵
a b←(⌊,⌈)⍵
x←↑2,/(a+0 int_prec)range b
sgn×+/0.5×int_prec×+/⍤0⊢x
}
⍝ The tanh-sinh quadrature.
tanh_sinh←{
>⍵:-⍵(⍺⍺∇∇)
⍵≡0 1:+/_tanh_sinh_m2ר_tanh_xk
a b← ⍵⋄g←
(b-a)×+/_tanh_sinh_m2×{g a+⍵×b-a}¨_tanh_xk
}
⍝ Some APLCart stuff I dislike grabbing over and over again.
median←2÷⍨1⊥⊢⌷⍨∘⊂⍋⌷⍨∘⊂∘⌈2÷⍨0 1+≢
stddev←≢÷⍨2*∘÷⍨(≢×+.*∘2)-2*⍨+⌿
diag←{⍵⊂⍤⊢⌸⍥,⍨+/↑⍳⍴⍵} ⍝ Antidiagonals as a vector of vectors.
⍝ Partition a n-element index array according to an invertible
⍝ complexity function.
part_f←{⌽⌽¨(⌽⍳⍵)⊂⍨⍸⍣¯1⌊⍣¯1⊢⍵}
⍝ Complexity functions. Used in the partitioning algoithm,
⍝ they include an additional n factor.
Onbang←⊢×! ⍝ O(n!)
Onlogn←××⍟ ⍝ O(n log n)
Ologn←⊢×⍟ ⍝ O(log n)
Osqrtn←⊢×(.5*⍨⊢) ⍝ O(sqrt(n))
On3←⊢*∘4 ⍝ O(n^3)
On2←⊢*∘3 ⍝ O(n^2)
On←×⍨ ⍝ O(n)
O1←⊢ ⍝ O(1)
⍝ A primitive approximation of limits at infinity.
lim_inf←{0:: ⍵⋄x←¨ 0 1+⍵⋄epsilon<|-/x:⍺⍺∇∇(1+⍵)⋄⊃x}
⍝ The error function.
erf←{_erf_c×0(*∘-×⍨)simpson⍵}
⍝ The sine integrals.
Si←{0 (1∘○÷⊢)simpson ⍵}
si←{(mm.Si ⍵)-○.5}
⍝ The cosine integrals.
Cin←{0 {⍵÷⍨1-2○⍵}simpson ⍵}
Ci←{mm.euler_gamma + (⍟-mm.Cin)⍵}
⍝ Offset logarithmic integral.
Li←{2 (÷∘⍟)mm.simpson ⍵}
⍝ Partial derivatives.
invariant_a←{⍵⍵ ⍵}
invariant_b←{⍵ ⍵⍵}
pderv_a←{epsilon÷⍨-/( invariant_b ⍵)¨⍺+epsilon 0} ⍝ Partial derivative df/d
pderv_b←{epsilon÷⍨-/( invariant_a )¨⍵+epsilon 0} ⍝ Partial derivative df/d⍵
⍝ The digamma function.
digamma←(!¯1∘+)derv÷(!¯1∘+)
⍝ Gradient vector.
nabla_grad←{( pderv_a, pderv_b)⍵}
:EndNamespace
sim←{
eunderspec←'Underspecified system. Missing the definition of nodes: '
enoint←'Failed to ensure integrity of the system'
epref←'Invalid prefix in specifier '
ea←' requires no arguments.' ' requires one argument.' ' requires two arguments.'
←0.5
code←{
(2⊃¨v/⍨x)@(⍸x←∊⊃¨v←⎕VFI¨⍵)⊢⍵
}¨' '(≠⊆⊢)¨⊃⎕NGET ⍵ 1
ind←⊃¨srt←code[⍋⊃¨code]
sys←{⍵/⍨(2|⎕DR)¨⍵}↑,/2↓¨srt
vrf←sys∊⍥∊ind
0=∧/vrf:(∊eunderspec(⍕sys/⍨~vrf))⎕SIGNAL 8
(≢≠⊃∘⌽)ind:enoint ⎕SIGNAL 8
leds←⍸(⊂'LED')≡¨2⊃¨srt
unpref←{'x'≠1↑⍵:(epref ⍵)⎕SIGNAL 8 ⋄ 1↓⍵}
load←{∊'v[',(⍕⍵),']'}
fmt←{{⍵/⍨(\∧∘⌽∨\∘⌽)' '≠⍵}∊('⍝'(≠⊆⊢)∊' '' '),¨⍵,⊂⍬}
state←⎕NS ⍬ ⋄ state.v←0⍨≢srt ⋄ state.t←0
arity←⊂'HIGH' 'LOW'
arity,←⊂'NOT' 'LED' 'BUTTON' 'CLOCK'
arity,←⊂'AND' 'OR' 'XOR' 'XNOR'
chka←{
0=/ind←(⊂⍺)∘∊¨arity:0
⍵≠¯1+⍸ind:(∊⍺,ea[⍸ind])⎕SIGNAL 8 ⋄ 1
}
src←∊'⋄'(1↓∘,,⍤0)(⊂'t+←1⋄⍬'),⍨{
var op args←2(↑,⊂⍤↓)⍵ ⋄ _←op chka≢args
av←load¨var,args
op≡'AND':'⍝←⍝∧⍝'fmt av⋄op≡'OR':'⍝←⍝∨⍝'fmt av
op≡'XOR':'⍝←⍝≠⍝'fmt av⋄op≡'XNOR':'⍝←⍝=⍝'fmt av
op≡'NOT':'⍝←~⍝'fmt av⋄op≡'HIGH':'⍝←1'fmt⊂av
op≡'LOW':'⍝←0'fmt⊂av⋄op≡'LED':''
op≡'BUTTON':'⍝←0≠⍝ t'fmt(⊂load var),args
op≡'CLOCK':'⍝←0=⍝|t'fmt(⊂load var),⊂unpref⊃args
(∊'unrecognised op 'op)⎕SIGNAL 8
}¨srt
⍺∘{
_←'state'⍎src
⎕←∊'Time: '(⍕state.t)' LEDs: '(⍕leds,¨state.v[3⊃¨srt[leds]])
⎕DL
}⍣{0}⊢⍬
}
dx←{
⍝ import dfns
dfns←{⍵⊣⍵.⎕CY'dfns'}⎕NS⍬
⍝ namespace for symbol implementations
ns←⎕NS ⍬
⍝ code page
cp←'⌶%⍵_abcdefghijklmnopqrstuvwxyz¯.'
cp,←'⍬0123456789⊢∆ABCDEFGHIJKLMNOPQRS'
cp,←'TUVWXYZ⍙[/⌿\⍀<≤=≥>≠∨∧-+÷×?∊"#&@:'
cp,←'⍷⋄←⍝)]~↑↓⍳○*⌈⌊∇∘(⊂⊃∩∪⊥⊤|;,⍱⍲⍒⍋⍉'
cp,←'⌽⊖⍟⌹!⍕⍎⍫⍪≡≢⎕⍞⍣⍭√⍛⍢⍍…⍙φ⍗⍐⍦⍁⍮Φ⍡'
cp,←∊(⎕UCS 32)(⎕UCS 10)
⍝ custom operations
t←⊂'~' '_Neg' ⋄ ns._Neg←{0=⍵}
t,←⊂'⍭' '_Pco' ⋄ ns._Pco←dfns.pco
t,←⊂'√' '_Root' ⋄ ns._Root←{←2 ⋄ ⍵*÷⍺}
t,←⊂'<' '_Lt' ⋄ ns._Lt←{0=⎕NC'':⍵-1 ⋄ <⍵}
t,←⊂'>' '_Gt' ⋄ ns._Gt←{0=⎕NC'':⍵+1 ⋄ >⍵}
t,←⊂'⍛' '_Rc' ⋄ ns._Rc←{⍵⍵∘⍺⍺}
t,←⊂'⍢' '_Round' ⋄ ns._Round←{←1 ⋄ (⊢∘×××∘⌈¯0.5+∘|÷⍨)⍵}
t,←⊂'⍍' '_MatMul' ⋄ ns._MatMul←{0=⎕NC'':(,⍨1,∘0)⍵ ⋄ +.×⍵}
t,←⊂'…' '_Range' ⋄ ns._Range←{
⍝ Adam Brudzewsky's Range function.
⍝ modified to follow a terser code style.
⎕IO←0 ⋄ c←0 2∊⍨10|⎕DR ⋄ t←1↓⍵ ⋄ d←c(e←⊃⍵)
f←⎕UCS⍣d⊢0 ⋄ ←f ⋄ l←-(2-d)⌊(≢⍺)⌊+/d=c¨¯2↑
s←l↓ ⋄ b←(¯1⌊l)↑¯2↑f,l↑ ⋄ d:s,t,⍨⎕UCS(⎕UCS b)∇ ⎕UCS e
F S←-⍨\2↑b,b+×e-b ⋄ s,t,⍨F+S×0⌈1+⌊(e-F)÷S+S=0
}
t,←⊂'⍙' '_MonadicDot' ⋄ ns._MonadicDot←{
⍝ https://dfns.dyalog.com/n_alt.htm
r c←
0=r:⍵⍵⌿,⍵
1≥c:⍺⍺⌿,⍵
M←~⍤1 0⍨r
⍵[;⎕IO].⍵⍵(∇⍤2)⍵[M;1↓c]
}
t,←⊂'φ' '_Fib' ⋄ ns._Fib←{←0 1 ⋄ 0=⍵:⊃⍺ ⋄ (1↓,+/)∇ ⍵-1}
t,←⊂'⍗' '_PowerDown' ⋄ ns._PowerDown←{op←⍣¯1 ⋄ 0=⎕NC'':op ⍵ ⋄ op ⍵}
t,←⊂'⍐' '_PowerUp' ⋄ ns._PowerUp←{op←⍣(¯1+2*31) ⋄ 0=⎕NC'':op ⍵ ⋄ op ⍵}
t,←⊂'∧' '_And' ⋄ ns._And←{0=⎕NC'':⍵[⍋⍵] ⋄ ⍺∧⍵}
t,←⊂'' '_Or' ⋄ ns._Or←{0=⎕NC'':⍵[⍒⍵] ⋄ ⍺∨⍵}
t,←⊂'⍦' '_Middle' ⋄ ns._Middle←{>0:(-)↓⍺↓⍵ ⋄ x←⌈(|)-⍨2÷⍨≢⍵ ⋄ (-x)↓x↓⍵}
t,←⊂'⍁' '_Diagonal' ⋄ ns._Diagonal←{⍵⊢∘⊂⌸⍨⍥,+/↑⍳⍴⍵}
t,←⊂'⍮' '_Pair' ⋄ ns._Pair←{
0≠⎕NC'':
⍵=0:'0123456789'
⍵=1:'abcdefghijklmnopqrstuvwxyz'
⍵=2:'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
⍵=3:'yaeiou'
⍵=4:'YAEIOU'
⍵=5:'YAEIOU'
⍵=6:819⌶⎕A~'YAEIOU'
⍵=7:⎕A~'YAEIOU'
⍵=8:4294967296
⍵=9:4294967295
⍵=10:2÷¯1+5*÷2 ⍝ golden ratio
⍵=11:⎕UCS 10
⍵=12:3.1415
}
t,←⊂'Φ' '_Totient' ⋄ ns._Totient←((×/⊢-≠)3∘dfns.pco)
t,←⊂'⍲' '_Prefixes' ⋄ ns._Prefixes←{0=⎕NC'':(⍳∘≢↑¨⊂)⍵ ⋄ ⍺⍲⍵}
t,←⊂'⍱' '_Suffixes' ⋄ ns._Suffixes←{0=⎕NC'':(⌽∘,¨,\∘⌽)⍵ ⋄ ⍺⍱⍵}
t,←⊂'=' '_Equal' ⋄ ns._Equal←{0=⎕NC'':(1=≢∘∪)⍵ ⋄ =⍵}
t,←⊂'⍡' '_Filter' ⋄ ns._Filter←{⍵/⍨⍺⍺ ⍵}
t,←⊂'' '_DownTack' ⋄ ns._DownTack←{←10⋄⊥⍣¯1⊢⍵}
t,←⊂'⌂' '_DX' ⋄ ns._DX←∇∇
⍝ translation and execution
k←,¯1↓⍉↑t ⋄ t3←{{0=≢⊃⍵:⊃⌽⍵ ⋄ ⊃⌽t⊃⍨⊃⍵}¨⍵,⍥⊂¨⍨{⍸∊k=⊃⍵}¨⍵}
g← ⋄ r←t3¨{⍵/⍨{0≠≢⍵}¨⍵}{{⍵↓⍨-'⍝'=⊃⊃⌽⍵}60⌶⍵}¨↓⎕CR'g'
p←↑↑{∊⍺'⋄'⍵}/{∊⍺' '⍵}/¨r
2=⎕NC'':(ns⍎p)⍵ ⋄ (ns⍎p)⍵
}
AddCentury←{
←→ Century Window, Century Anchor Year
⍝ ⍵ ←→Decimal time number 60
⍝ ← ←→ ⍵ with added century
f w t←
1∊'YYYY'⍷f:⍵
y←⌊⍵÷10*4
b←y<100
~/b:⍵
w>999:⍵+b×w×10*4
s x←0 100w-⍨t
c←b×s+y<x
⍵+c×10*6
}
BasicFormats←{
⍝ ⍵ ←→ Time Strings
⍝ ← ←→ Potential appropriate format strngs
m←↑⍵
w←1⊃m
toWide←w∘{⍵/⍨⍺≥≢¨⍵}
noDays←{⍵/⍨~'D'∊¨⍵}
hasDays←{⍵/⍨'D'∊¨⍵}
i←⊂¨⍋¨,33
sn←1∊∊(↓MonthNames'')⍷¨⊂⎕C m
fp←,i∘.⌷((4sn 1)/(4 3 2)(4 2 2)(2 3 2)2)/¨¨⊂'YMD'
fp←fp,fp~¨⊂⊂'DD'
fd←1↓¨∊¨'/',¨¨fp
fu←{⍵/⍨~/'MMM'⍷↑⍵}∊¨fp
v←i∘.⌷⊂'YMD'
v,←sn/(1+2×v='M')/¨v
v,←v~¨'D'
b←⎕D∊⍨' ',m
r←+/2</b ⍝ Digit runs
d←+/~b[0;] ⍝ Number of delims
fw←1=≢b ⍝ Fixed width
1∧.≥r:fu,⊂'excel' ⍝ toWide fu ⍝ No delimiters
fw∧(d=1)∧2∧.=r:noDays toWide fd ⍝ Fixed without days
fw∧(d=2)∧3∧.=r:hasDays toWide fd ⍝ Fixed with days
z←toWide fu,fd,v
z←z[⍒≢¨z] ⍝ Preference for longer format.
z[⍒+/¨'YMD'∘∊¨z] ⍝ Preference for more elements
}
CheckPattern←{
(⊂⎕C ⍵)∊'excel'∘,¨'' '1900' '1904':''
a←'YMDhms'
fe←'YMYMDhms'/¨⍨4 3,62
ve←'MMYMDhms'/¨⍨4 3,61
s←⍵⊆⍨+\2≠/' ',⍵
p←s/⍨a∊⍨⊃¨s
t←' in pattern ',⍵
0=≢p:'No elements provided',t
~∧/p∊fe,ve:'Invalid element',t
e←⊃¨p
(⊢≢∧\)a∊e:'Missing elements',t
e≢e:'Duplicate elements',t
f←/p∊'YY' 'YYYY'
f∧~∧/p∊fe:'Invalid (for fixed-width) element',t
(~f)∧~∧/p∊ve:'Invalid (for variable-width) element',t
''
}
ErrorCheck←{
0=⎕NC'':2 Signal'The function requires a left argument'
m←↑⍵
c←2=≢m ⍝ Is char data
n←1=≢m ⍝ Is Numeric datea
⍝ 80≠⎕DR m:11 Signal'The right argument must be a simple char mat or simple numeric vector'
f w t←ProcessLeftArgument
r←CheckPattern¨f
e←~r∊⊂''
/e:11 Signal⊃e/r
u←(∧/¨f∊¨⊂'YMDhms')(⎕C f)∊'excel'∘,¨'' '1900' '1904'
n∧~∧/u:11 Signal'Numeric data requires undelimited patterns.'
e←(~u)∧(1⊃2↑m)<≢¨f
/e:11 Signal'The format pattern ',(⊃e/f),' is wider than the right argument'
f←{1=≢⍵:⊃⍵ ⋄ ⍵}f
f w t m
}
FixedMMM←{
b←'MMM'⍷⍺
~1∊b:
i←⍸b
f←' '@i⊢
m←⍵
n←MonthNames''
m[;i+3]←'ZI3'⎕FMT 1+n⎕C m[;i+3]
f m
}
ProcessLeftArgument←{
w t←50,⊃⎕TS
80=⎕DR↑⍵:(⊆⍵)w t
3↑(⊂⊆⊃⍵),(1↓⍵),t
}
Text2Date←{
⍝ ⍵ ←→ Char mat of time strings
←→ Format(s) [Century Window] [Century Anchor Year]
⍝ ← ←→ ⎕DT time number type 60 or 0 for invalid
⍺←⊢
300+100::ReSignal ⎕DMX
(f w t m)←⍺ ErrorCheck ⍵
2=≡f:{⊃+/⍵×<\⍵≠0}({⍵ w t}¨f)∇¨⊂m
'excel'≡⎕C 5↑f:f Excel2Date m
k←+/'YMDhms'∊f
Validate f w t AddCentury k Default k Scale f Decode f Parse m
}
Validate←{
⍝ ⍵×60 0 ⎕DT ⍵
k←(0,5100)×10*6
f c←(1752 1 1 0 0 0)(4000 13 32 24 60 60)
l←(k[1;]=2)∧(0=4|k[0;])=(0=100|k[0;])=0=400|k[0;]
g←(k[2;]>0)∧k[2;]≤l+31 28 31 30 31 30 31 31 30 31 30 31[11⌊0⌈k[1;]-1]
⍵×(k[5;]=⌈k[5;])∧g∧∧⌿(f(≤⍤¯1)k)∧c(>⍤¯1)k
}
:Class Jarvis
⍝ Dyalog Web Service Server
⍝ See https://dyalog.github.io/Jarvis for documentation
(⎕ML ⎕IO)←1 1
∇ r←Config
⍝ returns current configuration
:Access public
r←↑{⍵(⍎⍵)}¨⎕THIS⍎'⎕NL ¯2.2 ¯2.1 ¯2.3'
∇ r←{value}DebugLevel level
⍝ monadic: return 1 if level is within Debug (powers of 2)
⍝ example: stopIf DebugLevel 2 ⍝ sets a stop if Debug contains 2
⍝ dyadic: return value unless level is within Debug (powers of 2)
⍝ example: :Trap 0 DebugLevel 5 ⍝ set Trap 0 unless Debug contains 1 or 4 in its
r←/(2 2 2⊃Debug).∧2 2 2level
:If 0≠⎕NC'value'
r←value/⍨~r
:EndIf
∇ {r}←{level}Log msg;ts
:Access public overridable
:If Logging>0∊msg
ts←fmtTS ⎕TS
:If 1=≢msg←⍕msg
:OrIf 1=⊃msg
r←ts,' - ',msg
:Else
r←ts,∊(⎕UCS 13),msg
:EndIf
⎕←r
:EndIf
∇ r←New arg
⍝ create a new instance of Jarvis
:Access public shared
:If 0∊arg
r←##.⎕NEW ⎕THIS
:Else
r←##.⎕NEW ⎕THIS arg
:EndIf
∇ make
:Access public
:Implements constructor
MakeCommon
∇ make1 args;rc;msg;char;t
:Access public
:Implements constructor
⍝ args is one of
⍝ - a simple character vector which is the name of a configuration file
⍝ - a reference to a namespace containing named configuration settings
⍝ - a depth 1 or 2 vector of
⍝ [1] integer port to listen on
⍝ [2] charvec function folder or ref to code location
⍝ [3] paradigm to use ('JSON' or 'REST')
MakeCommon
:If char←isChar args ⍝ character argument? it's either config filename or CodeLocation folder
:If ~⎕NEXISTS args
→0⊣Log'Unable to find "',args,'"'
:ElseIf 2=t←1 ⎕NINFO args ⍝ normal file
:If (lc⊢/⎕NPARTS args)∊'.json' '.json5' ⍝ json files are configuration
:If 0≠⊃(rc msg)←LoadConfiguration JarvisConfig←args
Log'Error loading configuration: ',msg
:EndIf
:Else
CodeLocation←args ⍝ might be a namespace script or class
:EndIf
:ElseIf 1=t ⍝ folder means it's CodeLocation
CodeLocation←args
:Else ⍝ not a file or folder
Log'Invalid constructor argument "',args,'"'
:EndIf
:ElseIf 9.1={⎕NC⊂,'⍵'}args ⍝ namespace?
:If 0≠⊃(rc msg)←LoadConfiguration args
Log'Error loading configuration: ',msg
:EndIf
:Else
:If 326=⎕DR args
:AndIf 0∧.=≡¨2↑args ⍝ if 2↑args is (port ref) (both scalar)
args[1]←⊂,args[1] ⍝ nest port so ∇default works properly
:EndIf
(Port CodeLocation Paradigm JarvisConfig)←args default Port CodeLocation Paradigm JarvisConfig
:EndIf
∇ MakeCommon
:Trap 11
JSONin←0 ##.##.⎕JSON⍠('Dialect' 'JSON5')('Format'JSONInputFormat)⊢ ⋄ {}JSONin'1'
JSONout←1 ##.##.⎕JSON⍠'HighRank' 'Split'⊢ ⋄ {}JSONout 1
JSONread←0 ##.##.⎕JSON⍠'Dialect' 'JSON5'⊢ ⍝ for reading configuration files
:Else
JSONin←0 ##.##.⎕JSON⍠('Format'JSONInputFormat)⊢
JSONout←1 ##.##.⎕JSON⊢
JSONread←0 ##.##.⎕JSON⊢
:EndTrap
∇ r←args default defaults
args←,⊆args
r←(≢defaults)↑args,(≢args)↓defaults
∇ Close
:Implements destructor
{0:: ⋄ {}LDRC.Close ServerName}⍬
∇ r←Run args;msg;rc
⍝ args is one of
⍝ - a simple character vector which is the name of a configuration file
⍝ - a reference to a namespace containing named configuration settings
⍝ - a depth 1 or 2 vector of
⍝ [1] integer port to listen on
⍝ [2] charvec function folder or ref to code location
⍝ [3] paradigm to use ('JSON' or 'REST')
:Access shared public
:Trap 0
(rc msg)←(r←New args).Start
:Else
(r rc msg)←'' ¯1 ⎕DMX.EM
:EndTrap
r←(r(rc msg))
∇ (rc msg)←Start;html;homePage;t
:Access public
:Trap 0 DebugLevel 1
Log'Starting ',⍕2↑Version
:If _started
:If 0(,2)≡LDRC.GetProp ServerName'Pause'
rc←1⊃LDRC.SetProp ServerName'Pause' 0
→0 If(rc'Failed to unpause server')
(rc msg)←0 'Server resuming operations'
→0
:EndIf
→0 If(rc msg)←¯1 'Server thinks it''s already started'
:EndIf
:If _stop
→0 If(rc msg)←¯1 'Server is in the process of stopping'
:EndIf
:If 'CLEAR WS'≡⎕WSID
:If ⎕NEXISTS JarvisConfig
:AndIf 2=⊃1 ⎕NINFO JarvisConfig
_rootFolder←⊃1 ⎕NPARTS JarvisConfig
:Else
_rootFolder←⊃1 ⎕NPARTS SourceFile
:EndIf
:Else
_rootFolder←⊃1 ⎕NPARTS ⎕WSID
:EndIf
→0 If(rc msg)←LoadConfiguration JarvisConfig
→0 If(rc msg)←CheckPort
→0 If(rc msg)←CheckCodeLocation
→0 If(rc msg)←Setup
→0 If(rc msg)←LoadConga
homePage←1 ⍝ default is to use built-in home page
:Select ⊃HTMLInterface
:Case 0 ⍝ explicitly no HTML interface, carry on
_htmlEnabled←0
:Case 1 ⍝ explicitly turned on
:If Paradigm≢'JSON'
Log'HTML interface is only available using JSON paradigm'
:Else
_htmlEnabled←1
:EndIf
:Case ¯1 ⍝ turn on if JSON paradigm
_htmlEnabled←Paradigm≡'JSON' ⍝ if not specified, HTML interface is enabled for JSON paradigm
:Else
:If 1<|≡HTMLInterface ⍝ is it '' 'function'?
t←2⊃HTMLInterface
:If 1 1 0≡⊃CodeLocation.⎕AT t
_htmlRootFn←t
_htmlEnabled←1
:Else
→0 If(rc msg)←¯1('HTML root function "',(⍕CodeLocation),'.',t,'" is not a monadic, result-returning function.')
:EndIf
:Else ⍝ otherwise it's 'file/folder'
_htmlEnabled←1
html←1 ⎕NPARTS((isRelPath HTMLInterface)/_rootFolder),HTMLInterface
:If isDir∊html
_htmlFolder←{⍵,('/'=⊢/⍵)↓'/'}∊html
:Else
_htmlFolder←1⊃html
_htmlDefaultPage←∊1↓html
:EndIf
homePage←⎕NEXISTS html←_htmlFolder,_htmlDefaultPage
Log(~homePage)/'HTML home page file "',(∊html),'" not found.'
:EndIf
:EndSelect
:If EnableCORS ⍝ if we've enabled CORS
:AndIf ¯1∊CORS_Methods ⍝ but not set any pre-flighted methods
:If Paradigm≡'JSON'
CORS_Methods←'GET,POST,OPTIONS' ⍝ allowed JSON methods are GET, POST, and OPTIONS
:Else
CORS_Methods←1↓∊',',¨RESTMethods[;1] ⍝ allowed REST methods are what the service supports
:EndIf
:EndIf
CORS_Methods←uc CORS_Methods
→0 If(rc msg)←StartServer
Log'Jarvis starting in "',Paradigm,'" mode on port ',⍕Port
Log'Serving code in ',(⍕CodeLocation),(CodeSource≢'')/' (populated with code from "',CodeSource,'")'
Log(_htmlEnabled∧homePage)/'Click http',(~Secure)↓'s://',MyAddr,':',(⍕Port),' to access web interface'
:Else ⍝ :Trap
(rc msg)←¯1 ⎕DMX.EM
:EndTrap
∇ (rc msg)←Stop;ts
:Access public
:If _stop
→0⊣(rc msg)←¯1 'Server is already stopping'
:EndIf
:If ~_started
→0⊣(rc msg)←¯1 'Server is not running'
:EndIf
ts←⎕AI[3]
_stop←1
Log'Stopping server...'
{0:: ⋄ {}LDRC.Close 2⊃LDRC.Clt'' ''Port'http'}''
:While ~_stopped
:If WaitTimeout<⎕AI[3]-ts
→0⊣(rc msg)←¯1 'Server seems stuck'
:EndIf
:EndWhile
(rc msg)←0 'Server stopped'
∇ (rc msg)←Pause
:Access public
→0 If~_started⊣(rc msg)←¯1 'Server is not running'
→0 If 2=⊃2⊃LDRC.GetProp ServerName'Pause'⊣(rc msg)←¯2 Error'Server is already paused'
→0 If 0≠rc←⊃LDRC.SetProp ServerName'Pause' 2⊣msg←'Error attempting to pause server'
Log'Pausing server...'
(rc msg)←0 'Server paused'
∇ (rc msg)←Reset
:Access Public
⎕TKILL _serverThread,_sessionThread,_taskThreads
_sessions←⍬
_sessionsInfo←0 50
_stopped←~_stop←_started←0
(rc msg)←0 'Server reset (previously set options are still in effect)'
∇ r←Running
:Access public
r←~_stopped
∇ (rc msg)←CheckPort;p
⍝ check for valid port number
:If DYALOG_JARVIS_PORT≢'' ⍝ environment variable takes precedence
Port←DYALOG_JARVIS_PORT
:EndIf
(rc msg)←3('Invalid port: ',∊⍕Port)
→0 If 0=p←⊃⊃(//)⎕VFI⍕Port
→0 If{(⍵>32767)(⍵<1)∨⍵≠⌊⍵}p
(rc msg)←0 ''
∇ (rc msg)←{force}LoadConfiguration value;config;public;set;file
:Access public
:If 0=⎕NC'force' ⋄ force←0 ⋄ :EndIf
(rc msg)←0 ''
→(_configLoaded>force)0 ⍝ did we already load from AutoStart?
:Trap 0 DebugLevel 1
:If isChar value
:If '#.'≡2↑value ⍝ check if a namespace reference
:AndIf 9.1=⎕NC⊂value
config←⍎value
→Load
:EndIf
file←JarvisConfig
:If ~0∊value
file←value
:EndIf
→0 If 0∊file
:If ⎕NEXISTS file
config←JSONread⊃⎕NGET file
:Else
→0⊣(rc msg)←6('Configuation file "',file,'" not found')
:EndIf
:ElseIf 9.1={⎕NC⊂,'⍵'}value ⍝ namespace?
config←value
:EndIf
Load:
public←⎕THIS⍎'⎕NL ¯2.2 ¯2.1 ¯2.3' ⍝ find all the public fields in this class
:If ~0∊set←public∩config.⎕NL ¯2 ¯9
config{⍎⍵,'←⍺⍎⍵'}¨set
:EndIf
_configLoaded←1
:Else
→0⊣(rc msg)←⎕DMX.EN ⎕DMX.('Error loading configuration: ',EM,(~0∊Message)/' (',Message,')')
:EndTrap
∇ (rc msg)←LoadConga;ref;root;nc;n;ns;congaCopied;class;path
⍝↓↓↓ Check if LDRC exists (VALUE ERROR (6) if not), and is LDRC initialized? (NONCE ERROR (16) if not)
(rc msg)←1 ''
:Hold 'JarvisInitConga'
:If {6 16 999::1 ⋄ ''≡LDRC:1 ⋄ 0⊣LDRC.Describe'.'}''
LDRC←''
:If ~0∊CongaRef ⍝ did the user supply a reference to Conga?
LDRC←ResolveCongaRef CongaRef
→∆END↓⍨0∊msg←(''≡LDRC)/'CongaRef (',(⍕CongaRef),') does not point to a valid instance of Conga'
:Else
:For root :In ##.## #
ref nc←root{1↑¨⍵{(×⍵)∘/¨⍺ ⍵}.⎕NC ⍵}ns←'Conga' 'DRC'
:If 9=⊃⌊nc ⋄ :Leave ⋄ :EndIf
:EndFor
:If 9=⊃⌊nc
LDRC←ResolveCongaRef root⍎∊ref
→∆END↓⍨0∊msg←(''≡LDRC)/(⍕root),'.',(∊ref),' does not point to a valid instance of Conga'
→∆COPY↓⍨{999::0 ⋄ 1⊣LDRC.Describe'.'}'' ⍝ it's possible that Conga was saved in a semi-initialized state
Log'Conga library found at ',(⍕root),'.',∊ref
:Else
∆COPY:
class←⊃⊃⎕CLASS ⎕THIS
congaCopied←0
:For n :In ns
:For path :In (1+0∊CongaPath)⊃(⊂CongaPath)((DyalogRoot,'ws/')'') ⍝ if CongaPath specified, use it exclusively
:Trap Debug↓0
n class.⎕CY path,'conga'
LDRC←ResolveCongaRef(class⍎n)
→∆END↓⍨0∊msg←(''≡LDRC)/n,' was copied from ',path,'conga but is not valid'
Log n,' copied from ',path,'conga'
→∆COPIED⊣congaCopied←1
:EndTrap
:EndFor
:EndFor
→∆END↓⍨0∊msg←(~congaCopied)/'Neither Conga nor DRC were successfully copied from [DYALOG]/ws/conga'
∆COPIED:
:EndIf
:EndIf
:EndIf
CongaVersion←0.1⊥2↑LDRC.Version
LDRC.X509Cert.LDRC←LDRC ⍝ reset X509Cert.LDRC reference
Log'Local Conga reference is ',⍕LDRC
rc←0
∆END:
:EndHold
∇ LDRC←ResolveCongaRef CongaRef;z;failed
⍝ Attempt to resolve what CongaRef refers to
⍝ CongaRef can be a charvec, reference to the Conga or DRC namespaces, or reference to an iConga instance
⍝ LDRC is '' if Conga could not be initialized, otherwise it's a reference to the the Conga.LIB instance or the DRC namespace
LDRC←'' ⋄ failed←0
:Select nameClass CongaRef ⍝ what is it?
:Case 9.1 ⍝ namespace? e.g. CongaRef←DRC or Conga
∆TRY:
:Trap 0 DebugLevel 1
:If /'.Conga'⍷⍕CongaRef ⋄ LDRC←CongaPath CongaRef.Init'Jarvis' ⍝ is it Conga?
:ElseIf 0≡⊃CongaRef.Init CongaPath ⋄ LDRC←CongaRef ⍝ DRC?
:Else ⋄ →∆EXIT⊣LDRC←''
:End
:Else ⍝ if Jarvis is reloaded and re-executed in rapid succession, Conga initialization may fail, so we try twice
:If failed ⋄ →∆EXIT⊣LDRC←''
:Else ⋄ →∆TRY⊣failed←1
:EndIf
:EndTrap
:Case 9.2 ⍝ instance? e.g. CongaRef←Conga.Init ''
LDRC←CongaRef ⍝ an instance is already initialized
:Case 2.1 ⍝ variable? e.g. CongaRef←'#.Conga'
:Trap 0 DebugLevel 1
LDRC←ResolveCongaRef(⍎∊⍕CongaRef)
:EndTrap
:EndSelect
∆EXIT:
∇ (rc msg secureParams)←CreateSecureParams;cert;certs;msg;inds
⍝ return Conga parameters for running HTTPS, if Secure is set to 1
LDRC.X509Cert.LDRC←LDRC ⍝ make sure the X509 instance points to the right LDRC
(rc secureParams msg)←0 ⍬''
:If Secure
:If ~0∊RootCertDir ⍝ on Windows not specifying RootCertDir will use MS certificate store
→∆EXIT If(rc msg)←'RootCertDir'Exists RootCertDir
→∆EXIT If(rc msg)←{(⊃⍵)'Error setting RootCertDir'}LDRC.SetProp'.' 'RootCertDir'RootCertDir
⍝ The following is commented out because it seems the GnuTLS knows to use the operating system's certificate collection even on non-Windows platforms
⍝ :ElseIf ~isWin
⍝ →∆EXIT⊣(rc msg)←¯1 'No RootCertDir spcified'
:EndIf
:If 0∊ServerCertSKI ⍝ no certificate ID specified, check for Cert and Key files
→∆EXIT If(rc msg)←'ServerCertFile'Exists ServerCertFile
→∆EXIT If(rc msg)←'ServerKeyFile'Exists ServerKeyFile
:Trap 0 DebugLevel 1
cert←⊃LDRC.X509Cert.ReadCertFromFile ServerCertFile
:Else
(rc msg)←⎕DMX.EN('Unable to decode ServerCertFile "',(∊⍕ServerCertFile),'" as a certificate')
→∆EXIT
:EndTrap
cert.KeyOrigin←'DER'ServerKeyFile
:ElseIf isWin ⍝ ServerCertSKI only on Windows
certs←LDRC.X509Cert.ReadCertUrls
:If 0∊certs
→∆EXIT⊣(rc msg)←8 'No certificates found in Microsoft Certificate Store'
:Else
inds←1+('id=',ServerCertSKI,';')⎕S{⍵.BlockNum}⍠'Greedy' 0⊢2⊃¨certs.CertOrigin
:If 1≠≢inds
rc←9
msg←(0 2⍸≢inds)⊃('Certificate with id "',ServerCertSKI,'" was not found in the Microsoft Certificate Store')('There is more than one certificate with Subject Key Identifier "',ServerCertSKI,'" in the Microsoft Certificate Store')
→∆EXIT
:EndIf
cert←certs[⊃inds]
:EndIf
:Else ⍝ ServerCertSKI is defined, but we're not running Windows
→∆EXIT⊣(rc msg)←10 'ServerCertSKI is currently valid only under Windows'
:EndIf
secureParams←('X509'cert)('SSLValidation'SSLValidation)('Priority'Priority)
:EndIf
∆EXIT:
∇ (rc msg)←CheckCodeLocation;root;m;res;tmp;fn;path
(rc msg)←0 ''
:If DYALOG_JARVIS_CODELOCATION≢'' ⍝ environment variable take precedence
CodeLocation←DYALOG_JARVIS_CODELOCATION
:EndIf
:If 0∊CodeLocation
:If 0∊JarvisConfig ⍝ if there's a configuration file, use its folder for CodeLocation
→0⊣(rc msg)←4 'CodeLocation is empty!'
:Else
CodeLocation←⊃1 ⎕NPARTS JarvisConfig
:EndIf
:EndIf
:Select ⊃{⎕NC'⍵'}CodeLocation ⍝ need dfn because CodeLocation is a field and will always be nameclass 2
:Case 9 ⍝ reference, just use it
:Case 2 ⍝ variable, could be file path or ⍕ of reference from JarvisConfig
:If 326=⎕DR tmp←{0::⍵ ⋄ '#'≠⊃⍵:⍵ ⋄ ⍎⍵}CodeLocation
:AndIf 9={⎕NC'⍵'}tmp ⋄ CodeLocation←tmp
:Else
root←(isRelPath CodeLocation)/_rootFolder
path←∊1 ⎕NPARTS root,CodeLocation
:Trap 0 DebugLevel 1
:If 1=t←1 ⎕NINFO path ⍝ folder?
CodeLocation←⍎'CodeLocation'#.⎕NS''
_codeSource←path
→0 If(rc msg)←CodeLocation LoadFromFolder path
:ElseIf 2=t ⍝ file?
CodeLocation←#.⎕FIX'file://',path
_codeSource←path
:Else
→0⊣(rc msg)←5('CodeLocation "',(∊⍕CodeLocation),'" is not a folder or script file.')
:EndIf
:Case 22 ⍝ file name error
→0⊣(rc msg)←6('CodeLocation "',(∊⍕CodeLocation),'" was not found.')
:Else ⍝ anything else
→0⊣(rc msg)←7((⎕DMX.(EM,' (',Message,') ')),'occured when validating CodeLocation "',(∊⍕CodeLocation),'"')
:EndTrap
:EndIf
:Else
→0⊣(rc msg)←5 'CodeLocation is not valid, it should be either a namespace/class reference or a file path'
:EndSelect
:For fn :In AppInitFn AppCloseFn ValidateRequestFn AuthenticateFn SessionInitFn~⊂''
:If 3≠CodeLocation.⎕NC fn
msg,←(0∊msg)↓',"CodeLocation.',fn,'" was not found '
:EndIf
:EndFor
→0 If rc←8×~0∊msg
:If ~0∊AppInitFn ⍝ initialization function specified?
:Select ⊃CodeLocation.⎕AT AppInitFn
:Case 1 0 0 ⍝ result-returning niladic?
stopIf DebugLevel 2
res←CodeLocation⍎AppInitFn ⍝ run it
:Case 1 1 0 ⍝ result-returning monadic?
stopIf DebugLevel 2
res←(CodeLocation⍎AppInitFn)⎕THIS ⍝ run it
:Else
→0⊣(rc msg)←8('"',(⍕CodeLocation),'.',AppInitFn,'" is not a niladic or monadic result-returning function')
:EndSelect
:If 0≠⊃res
→0⊣(rc msg)←2↑res,(≢res)↓¯1('"',(⍕CodeLocation),'.',AppInitFn,'" did not return a 0 return code')
:EndIf
:EndIf
:If ~0∊AppCloseFn ⍝ application close function specified?
:If 1 0 0≢⊃CodeLocation.⎕AT AppCloseFn ⍝ result-returning niladic?
→0⊣(rc msg)←8('"',(⍕CodeLocation),'.',AppCloseFn,'" is not a niladic result-returning function')
:EndIf
:EndIf
Validate←{0} ⍝ dummy validation function
:If ~0∊ValidateRequestFn ⍝ Request validation function specified?
:If ∧/(⊃CodeLocation.⎕AT ValidateRequestFn)∊¨1(1 ¯2)0 ⍝ result-returning monadic or ambivalent?
Validate←CodeLocation⍎ValidateRequestFn
:Else
→0⊣(rc msg)←8('"',(⍕CodeLocation),'.',ValidateRequestFn,'" is not a monadic result-returning function')
:EndIf
:EndIf
Authenticate←{0} ⍝ dummy authentication function
:If ~0∊AuthenticateFn ⍝ authentication function specified?
:If ∧/(⊃CodeLocation.⎕AT AuthenticateFn)∊¨1(1 ¯2)0 ⍝ result-returning monadic or ambivalent?
Authenticate←CodeLocation⍎AuthenticateFn
:Else
→0⊣(rc msg)←8('"',(⍕CodeLocation),'.',AuthenticateFn,'" is not a monadic result-returning function')
:EndIf
:EndIf
∇ (rc msg)←Setup
⍝ perform final setup before starting server
(rc msg)←0 ''
Paradigm←uc Paradigm
:Select Paradigm
:Case 'JSON'
RequestHandler←HandleJSONRequest
:Case 'REST'
RequestHandler←HandleRESTRequest
:If 2>≢RESTMethods
RESTMethods←↑2¨'/'(≠⊆⊢)¨','(≠⊆⊢),RESTMethods
:EndIf
:Else
(rc msg)←¯1 'Invalid paradigm'
:EndSelect
Exists←{0:: ¯1 (,' "',⍵,'" is not a valid folder name.') ⋄ ⎕NEXISTS ⍵:0 '' ⋄ ¯1 (,' "',⍵,'" was not found.')}
∇ (rc msg)←StartServer;r;cert;secureParams;accept;deny;mask;certs;options
msg←'Unable to start server'
accept←'Accept'ipRanges AcceptFrom
deny←'Deny'ipRanges DenyFrom
→∆EXIT If⊃(rc msg secureParams)←CreateSecureParams
{}LDRC.SetProp'.' 'EventMode' 1 ⍝ report Close/Timeout as events
options←''
:If 3.3≤CongaVersion ⍝ can we set DecodeBuffers at server creation?
options←⊂'Options' 5 ⍝ DecodeBuffers + WSAutoAccept
:EndIf
:If 3.4≤CongaVersion ⍝ DOSLimit support started with v3.4
:AndIf DOSLimit≠¯1 ⍝ not using Conga's default value
:If 0≠⊃LDRC.SetProp'.' 'DOSLimit'DOSLimit
→∆EXIT⊣(rc msg)←¯1 'Invalid DOSLimit setting: ',∊⍕DOSLimit
:EndIf
:EndIf
_connections←⎕NS''
_connections.index←2 0'' 0 ⍝ row-oriented for faster lookup
_connections.lastCheck←0
:If 0=rc←1⊃r←LDRC.Srv ServerName''Port'http'BufferSize,secureParams,accept,deny,options
ServerName←2⊃r
:If 3.3>CongaVersion
{}LDRC.SetProp ServerName'FIFOMode' 0 ⍝ deprecated in Conga v3.2
{}LDRC.SetProp ServerName'DecodeBuffers' 15 ⍝ 15 ⍝ decode all buffers
{}LDRC.SetProp ServerName'WSFeatures' 1 ⍝ auto accept WS requests
:EndIf
:If 0∊Hostname ⍝ if Host hasn't been set, set it to the default
Hostname←'http',(~Secure)↓'s://',(2 ⎕NQ'.' 'TCPGetHostID'),((~Port∊80 443)/':',⍕Port),'/'
:EndIf
InitSessions
(rc msg)←RunServer
:Else
Log msg←'Error ',(⍕rc),' creating server',(rc∊98 10048)/': port ',(⍕Port),' is already in use' ⍝ 98=Linux, 10048=Windows
:EndIf
∆EXIT:
∇ (rc msg)←RunServer;thread
thread←lc,⍕DYALOG_JARVIS_THREAD
:If (⊂thread)∊'' 'auto'
:If InTerm ⍝ do we have an interactive terminal?
thread←'debug'
:Else
thread←,'1'
:EndIf
:EndIf
:Select thread
:Case ,'0' ⍝ Run in thread 0
(rc msg)←Server''
QuadOFF
:Case ,'1' ⍝ Run in non-0 thread, use ⎕TSYNC
(rc msg)←⎕TSYNC _serverThread←Server&⍬
QuadOFF
:Case 'debug'
_serverThread←Server&⍬
(rc msg)←0 'Server started'
:Else
(rc msg)←¯1 'Invalid setting for DYALOG_JARVIS_THREAD'
:EndSelect
∇ {r}←Server arg;wres;rc;obj;evt;data;ref;ip;msg;tmp;conx
(_started _stopped)←1 0
:While ~_stop
:Trap 0 DebugLevel 1
wres←LDRC.Wait ServerName WaitTimeout ⍝ Wait for WaitTimeout before timing out
⍝ wres: (return code) (object name) (command) (data)
(rc obj evt data)←4↑wres
conx←obj(⍳↓⊣)'.'
:Select rc
:Case 0
:Select evt
:Case 'Error'
_stop←ServerName≡obj ⍝ if we got an error on the server itself, signal to stop
:If 0≠4⊃wres
Log'Server: DRC.Wait reported error ',(⍕4⊃wres),' on ',(2⊃wres),GetIP obj
:EndIf
RemoveConnection conx ⍝ Conga closes object on an Error event
:Case 'Connect'
AddConnection conx
:CaseList 'HTTPHeader' 'HTTPTrailer' 'HTTPChunk' 'HTTPBody'
:If 0≠_connections.⎕NC conx
ref←_connections⍎conx
_taskThreads←⎕TNUMS∩_taskThreads,ref{ HandleRequest ⍵}&wres
ref.Time←⎕AI[3]
:Else
Log'Server: Object ''_connections.',conx,''' was not found.'
{0:: ⋄ {}LDRC.Close ⍵}obj
:EndIf
:Case 'Closed'
RemoveConnection conx
:Case 'Timeout'
:Else ⍝ unhandled event
Log'Server: Unhandled Conga event:'
Log⍕wres
:EndSelect ⍝ evt
:Case 1010 ⍝ Object Not found
:If ~_stop
Log'Server: Object ''',ServerName,''' has been closed - Jarvis shutting down'
_stop←1
:EndIf
:Else
Log'Server: Conga wait failed:'
Log wres
:EndSelect ⍝ rc
CleanupConnections
:Else ⍝ :Trap
Log'*** Server error ',msg←1 ⎕JSON⍠'Compact' 0⊢⎕DMX
r←¯1 msg
→Exit
:EndTrap
:EndWhile
r←0 'Server stopped'
Exit:
:If ~0∊AppCloseFn
r←CodeLocation⍎AppCloseFn
:EndIf
Close
⎕TKILL _sessionThread
(_stop _started _stopped)←0 0 1
∇ AddConnection conx
:Hold '_connections'
conx _connections.⎕NS''
_connections.index,←conx(⎕AI[3])
(_connections⍎conx).IP←2⊃2⊃LDRC.GetProp obj'PeerAddr'
:EndHold
∇ RemoveConnection conx
:Hold '_connections'
_connections.⎕EX conx
_connections.index/⍨←_connections.index[1;]≢¨⊂conx
:EndHold
∇ CleanupConnections;conxNames;timedOut;dead;kids;connecting;connected
:If _connections.lastCheck<⎕AI[3]-ConnectionTimeout×1000
:Hold '_connections'
connecting←connected←⍬
:If ~0∊kids←2 2⊃LDRC.Tree ServerName ⍝ retrieve children of server
⍝ LDRC.Tree
⍝ connecting → status 3 1 - incoming connection
⍝ connected → status 3 4 - connected connection
(connecting connected)←2↑{((2 23 1 3 4)⍪⍵[;2 3]){⊂1↓⍵}⌸'' '',⍵[;1]}↑⊃¨kids
:EndIf
conxNames←_connections.index[1;]~connecting
timedOut←_connections.index[1;]/⍨ConnectionTimeout<0.001×⎕AI[3]-_connections.index[2;]
:If /{~0∊⍵}¨connected conxNames
:If ~0∊timedOut
timedOut/⍨←{6::1 ⋄ 0=(_connections⍎⍵).⎕NC⊂'Req'}¨timedOut
:EndIf
dead←(connected~conxNames),timedOut ⍝ (connections not in the index), timed out
{0∊⍵: ⋄ {}LDRC.Close ServerName,'.',⍵}¨dead ⍝ attempt to close them
⍝ remove timed out, or connections that are
_connections.⎕EX(conxNames~connected~dead),timedOut
_connections.index/⍨←_connections.index[1;]∊_connections.⎕NL ¯9
:EndIf
_connections.lastCheck←⎕AI[3]
:EndHold
:EndIf
:Section RequestHandling
∇ r←ErrorInfo
:Trap 0
r←⍕ErrorInfoLevel↑⎕DMX.(EM({⍵↑⍨⍵⍳']'}2⊃DM))
:Else
r←''
:EndTrap
∇ req←MakeRequest args
⍝ create a request, use MakeRequest '' for interactive debugging
⍝ :Access public ⍝ uncomment for debugging
:If 0∊args
req←⎕NEW Request
:Else
req←⎕NEW Request args
:EndIf
req.(Server ErrorInfoLevel)←⎕THIS ErrorInfoLevel
∇ ns HandleRequest req;data;evt;obj;rc;cert;fn
(rc obj evt data)←req ⍝ from Conga.Wait
:Hold obj
:Select evt
:Case 'HTTPHeader'
ns.Req←MakeRequest data
ns.Req.PeerCert←''
ns.Req.PeerAddr←2⊃2⊃LDRC.GetProp obj'PeerAddr'
ns.Req.Server←⎕THIS
:If Secure
(rc cert)←2↑LDRC.GetProp obj'PeerCert'
:If rc=0
ns.Req.PeerCert←cert
:Else
ns.Req.PeerCert←'Could not obtain certificate'
:EndIf
:EndIf
:Case 'HTTPBody'
ns.Req.ProcessBody data
:Case 'HTTPChunk'
ns.Req.ProcessChunk data
:Case 'HTTPTrailer'
ns.Req.ProcessTrailer data
:EndSelect
:If ns.Req.Complete
:Select lc ns.Req.GetHeader'content-encoding' ⍝ zipped request?
:Case '' ⍝ no encoding
:If ns.Req.Charset≡'utf-8'
ns.Req.Body←'UTF-8'⎕UCS ⎕UCS ns.Req.Body
:EndIf
:Case 'gzip'
ns.Req.Body←⎕UCS 256|¯3 Zipper 83 ⎕DR ns.Req.Body
:Case 'deflate'
ns.Req.Body←⎕UCS 256|¯2 Zipper 83 ⎕DR ns.Req.Body
:Else
→resp⊣'Unsupported content-encoding'ns.Req.Fail 400
:EndSelect
:If _htmlEnabled∧ns.Req.Response.Status≠200
ns.Req.Response.Headers←1 2'Content-Type' 'text/html; charset=utf-8'
ns.Req.Response.Payload←'<h3>',(⍕ns.Req.Response.((⍕Status),' ',StatusText)),'</h3>'
→resp
:EndIf
⍝ Application-specified validation
stopIf DebugLevel 4+2×~0∊ValidateRequestFn
rc←Validate ns.Req
ns.Req.Fail 400×(ns.Req.Response.Status=200)∧0≠rc ⍝ default status 400 if not set by application
→resp If rc≠0
fn←1↓'.'@('/'∘=)ns.Req.Endpoint
fn RequestHandler ns ⍝ RequestHandler is either HandleJSONRequest or HandleRESTRequest
resp: obj Respond ns
:EndIf
:EndHold
∇ fn HandleJSONRequest ns;payload;resp;valence;nc;debug;file;isGET
→handle If~isGET←'get'≡ns.Req.Method
:If AllowGETs ⍝ if we allow GETs
:AndIf ~'.'∊ns.Req.Endpoint ⍝ and the endpoint doesn't have a '.' (file extension)
→handle If 3=⌊|{0::0 ⋄ CodeLocation.⎕NC⊂⍵}fn ⍝ handle it if there's a matching function for the endpoint
:EndIf
→End If'Request method should be POST'ns.Req.Fail 405×~_htmlEnabled
→handleHtml If~0∊_htmlFolder
ns.Req.Response.Headers←1 2'Content-Type' 'text/html; charset=utf-8'
ns.Req.Response.Payload←'<!DOCTYPE html><html><head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"><link rel="icon" href="data:,"></head><body><h2>400 Bad Request</h2></body></html>'
→End If'Bad URI'ns.Req.Fail 400×~0∊fn ⍝ either fail with a bad URI or exit if favicon.ico (no-op)
:If 0∊_htmlRootFn
ns.Req.Response.Payload←HtmlPage
:Else
ns.Req.Response.Payload←{1 CodeLocation.(85⌶)_htmlRootFn,' ⍵'}ns.Req
:EndIf
→End
handleHtml:
:If (,'/')≡ns.Req.Endpoint
file←_htmlFolder,_htmlDefaultPage
:Else
file←_htmlFolder,('/'=⊣/ns.Req.Endpoint)↓ns.Req.Endpoint
:EndIf
file←∊1 ⎕NPARTS file
file,←(isDir file)/'/',_htmlDefaultPage
→End If ns.Req.Fail 400×~_htmlFolder begins file
:If 0≠ns.Req.Fail 404×~⎕NEXISTS file
→End If 0=Report404InHTML
ns.Req.Response.Headers←1 2'Content-Type' 'text/html; charset=utf-8'
ns.Req.Response.Payload←'<h3>Not found: ',(file↓⍨≢_htmlFolder),'</h3>'
→End
:EndIf
ns.Req.Response.Payload←''file
'Content-Type'ns.Req.DefaultHeader ns.Req.ContentTypeForFile file
→End
handle:
→End If HandleCORSRequest ns.Req
→End If'No function specified'ns.Req.Fail 400×0∊fn
→End If'Unsupported request method'ns.Req.Fail 405×(⊂ns.Req.Method)(~∊)(~AllowGETs)↓'get' 'post'
→End If'Cannot accept query parameters'ns.Req.Fail 400×AllowGETs⍱0∊ns.Req.QueryParams
:Select ns.Req.ContentType
:Case 'application/json'
:Trap 0 DebugLevel 1
ns.Req.Payload←{0∊⍵:⍵ ⋄ JSONin ⍵}ns.Req.Body
:Else
→End⊣'Could not parse payload as JSON'ns.Req.Fail 400
:EndTrap
:Case 'multipart/form-data'
→End If'Content-Type should be "application/json"'ns.Req.Fail 400×~AllowFormData
:Trap 0 DebugLevel 1
ns.Req.Payload←ParseMultipartForm ns.Req
:Else
→End⊣'Could not parse payload as "multipart/form-data"'ns.Req.Fail 400
:EndTrap
:Case ''
→End If'No Content-Type specified'ns.Req.Fail 400×~isGET∧AllowGETs
:Trap 0 DebugLevel 1
:If 0∊ns.Req.QueryParams
ns.Req.Payload←''
:ElseIf 1=≢ns.Req.QueryParams ⍝ name/value pairs
ns.Req.Payload←JSONin ns.Req.QueryParams
:Else
ns.Req.Payload←{JSONin{1⌽'}{',¯1↓∊'"',¨⍵[;,1],¨'":'∘,¨⍵[;,2],¨','}⍵}ns.Req.QueryParams
:EndIf
:Else
→0⊣'Could not parse query string as JSON'ns.Req.Fail 400
:EndTrap
:Else
→0⊣('Content-Type should be "application/json"',AllowFormData/' or "multipart/form-data"')ns.Req.Fail 400
:EndSelect
→End If CheckAuthentication ns.Req
→End If('Invalid function "',fn,'"')ns.Req.Fail CheckFunctionName fn
→End If('Invalid function "',fn,'"')ns.Req.Fail 404×3≠⌊|{0::0 ⋄ CodeLocation.⎕NC⊂⍵}fn ⍝ is it a function?
valence←|⊃CodeLocation.⎕AT fn
nc←CodeLocation.⎕NC⊂fn
→End If('"',fn,'" is not a monadic result-returning function')ns.Req.Fail 400×(1 1 0≢×valence)>(0∧.=valence)∧3.3=nc
resp←''
:Trap 0 DebugLevel 1
:Trap 85
:If (2=valence[2])>3.3=nc ⍝ dyadic and not tacit
stopIf DebugLevel 2
resp←ns.Req{0 CodeLocation.(85⌶)' ',fn,' ⍵'}ns.Req.Payload ⍝ intentional stop for application-level debugging
:Else
stopIf DebugLevel 2
resp←{0 CodeLocation.(85⌶)fn,' ⍵'}ns.Req.Payload ⍝ intentional stop for application-level debugging
:EndIf
:Else ⍝ no result from the endpoint
:If 0∊ns.Req.Response.Payload ⍝ no payload?
:AndIf 200=ns.Req.Response.Status ⍝ endpoint did not change the status
→End⊣ns.Req.Fail 204 ⍝ no content
:EndIf
:EndTrap
:Else
→End⊣ErrorInfo ns.Req.Fail 500
:EndTrap
→End If 204=ns.Req.Response.Status
⍝ Exit if
⍝ ↓↓↓↓↓↓↓ no response from endpoint,
⍝ and ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ endpoint did not set payload
⍝ and ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ endpoint did not fail the request
→End If(0∊resp)∧(0∊ns.Req.Response.Payload)∧200≠ns.Req.Response.Status
'Content-Type'ns.Req.DefaultHeader DefaultContentType ⍝ set the header if not set
:If /'application/json'⍷ns.Req.(Response.Headers GetHeader'content-type') ⍝ if the response is JSON
ns.Req.Response ToJSON resp ⍝ convert it
:Else
ns.Req.Response.Payload←resp
:EndIf
:If 0∊ns.Req.Response.Payload
'Content-Length'ns.Req.DefaultHeader 0
:EndIf
End:
∇ formData←ParseMultipartForm req;boundary;body;part;headers;payload;disposition;type;name;filename;tmp
boundary←crlf,'--',req.Boundary ⍝ the HTTP standard prepends '--' to the boundary
body←req.Body
formData←⎕NS''
body←⊃body splitOnFirst boundary,'--' ⍝ drop off trailing boundary ('--' is appended to the trailing boundary)
:For part :In (crlf,body)splitOn boundary ⍝ split into parts
(headers payload)←part splitOnFirst crlf,crlf
(disposition type)←deb¨2↑headers splitOn crlf
(name filename)←deb¨2↑1↓disposition splitOn';'
name←'"'~⍨2⊃name splitOn'='
tmp←⎕NS''
:If {¯1=⎕NC ⍵}name
→0⊣'Invalid form field name for Jarvis'req.Fail 400
:EndIf
filename←'"'~⍨2⊃2↑filename splitOn'='
tmp.(Name Filename)←name filename
tmp.Content←payload
tmp.Content_Type←deb 2⊃2↑type splitOn':'
:If 0=formData.⎕NC name ⋄ formData{⍺⍎⍵,'←⍬'}name ⋄ :EndIf
formData(name{⍺⍎⍺⍺,',←⍵'})tmp
:EndFor
∇ fn HandleRESTRequest ns;ind;exec;valence;ct;resp
→0 If HandleCORSRequest ns.Req
→0 If CheckAuthentication ns.Req
:If ParsePayload
:Trap 0 DebugLevel 1
:Select ns.Req.ContentType
:Case 'application/json'
ns.Req.Payload←JSONin ns.Req.Body
:Case 'application/xml'
ns.Req.(Payload←⎕XML Body)
:EndSelect
:Else
→0⊣('Unable to parse request body as ',ct)ns.Req.Fail 400
:EndTrap
:EndIf
ind←RESTMethods[;1](nocase)⊂ns.Req.Method
→0 If ns.Req.Fail 405×(≢RESTMethods)<ind
exec←⊃RESTMethods[ind;2]
→0 If ns.Req.Fail 501×0∊exec
resp←''
:Trap 0 DebugLevel 1
:Trap 85
stopIf DebugLevel 2
resp←{1 CodeLocation.(85⌶)exec,' ⍵'}ns.Req ⍝ intentional stop for application-level debugging
:EndTrap
:Else
→0⊣ns.Req.Fail 500
:EndTrap
→0 If 2≠⌊0.01×ns.Req.Response.Status
:If (ns.Req.(Response.Headers GetHeader'content-type')≡'')∧~0∊DefaultContentType
'content-type'ns.Req.SetHeader DefaultContentType
:EndIf
:If 'application/json'match⊃';'(≠⊆⊢)ns.Req.(Response.Headers GetHeader'content-type')
ns.Req.Response ToJSON resp
:EndIf
∇ r←HandleCORSRequest req;origin;reflect
r←0
→0 If~EnableCORS
→0 If 0∊origin←req.GetHeader'Origin' ⍝ CORS requests have an Origin header
reflect←{(1+(,)≡,1)⊃⍺ ⍵} ⍝ if CORS_xxx setting is 1, reflect the request's value
'Access-Control-Allow-Origin'req.DefaultHeader CORS_Origin reflect origin
→0 If~req.Method≡'options' ⍝ OPTIONS (with an Origin header) indicates a "pre-flighted" CORS request
→0 If 0∊req.GetHeader'Access-Control-Request-Method' ⍝
'Access-Control-Allow-Methods'req.DefaultHeader CORS_Methods reflect req.GetHeader'Access-Control-Request-Method'
'Access-Control-Allow-Headers'req.DefaultHeader CORS_Headers reflect req.GetHeader'Access-Control-Request-Headers'
'Access-Control-Max-Age'req.DefaultHeader(⍕CORS_MaxAge)
req.SetStatus 204 ⍝ No Content
r←1
∇ response ToJSON data
⍝ convert APL response payload to JSON
:Trap 0 DebugLevel 1
response.Payload←⎕UCS SafeJSON JSONout data
:Else
'Could not format result payload as JSON'ns.Req.Fail 500
:EndTrap
∇ w←SafeJSON w;i;c;⎕IO
⍝ Convert Unicode chars to \uXXXX
⎕IO←0
→0⍨0∊i←⍸127<c←⎕UCS w
w[i]←{⊂'\u','0123456789ABCDEF'[16 16 16 16⍵]}¨c[i]
w←∊w
∇ r←CheckAuthentication req
⍝ Check request authentication
⍝ r is 0 if request processing can continue
r←1
:If 0=DoAuthentication req ⍝ might still want to do some authentication
:If 0≠SessionTimeout ⍝ using sessions?
:If 0≠CheckSession req ⍝ session is still valid?
CreateSession req
:EndIf
:EndIf
r←0
:EndIf
∇ rc←DoAuthentication req;debug;old
⍝ rc is 0 if either no authentication is required or authentication succeeds
rc←0
:Trap 0 DebugLevel 1
stopIf DebugLevel 2×~0∊AuthenticateFn
rc←Authenticate req ⍝ intentional stop for application-level debugging
:If rc≠0
req.Fail 401
:If HTTPAuthentication match'basic'
'WWW-Authenticate'req.SetHeader'Basic realm="Jarvis", charset="UTF-8"'
:EndIf
:EndIf
:Else ⍝ Authenticate errored
(⎕DMX.EM,' occured during authentication')req.Fail 500
rc←1
:EndTrap
∇ obj Respond ns;status;z;res;close;conx
res←ns.Req.Response
status←(⊂ns.Req.HTTPVersion),res.((⍕Status)StatusText)
res.Headers⍪←'Server'(deb⍕2↑Version)
res.Headers⍪←'Date'(2⊃LDRC.GetProp'.' 'HttpDate')
conx←lc ns.Req.GetHeader'connection'
close←(('HTTP/1.0'≡ns.Req.HTTPVersion)>'keep-alive'≡conx)'close'≡conx
close←2≠⌊0.01×res.Status ⍝ close the connection on non-2XX status
UseZip ContentEncode ns.Req
:Select 1⊃z←LDRC.Send obj(status,res.Headers res.Payload)close
:Case 0 ⍝ everything okay, nothing to do
:Case 1008 ⍝ Wrong object class likely caused by socket being closed during the request
⍝ do nothing for now
:Else
Log'Respond: Conga error when sending response',GetIP obj
Log⍕z
:EndSelect
ns.⎕EX'Req'
∇ UseZip ContentEncode req;enc
→End If 0=UseZip ⍝ is zipping enabled?
→End If 0∊enc←req.AcceptEncodings ⍝ does the client accept zipped responses?
:If UseZip≤≢req.Response.Payload ⍝ payload exceeds size threshhold?
:Select ⊃enc
:Case 'gzip'
:Trap 0
req.Response.Payload←2⊃3 ZipLevel Zipper sint req.Response.Payload
:Else
Log'ContentEncode: gzip content-encoding failed'
→End
:EndTrap
'Content-Encoding'req.SetHeader'gzip'
:Case 'deflate'
:Trap 0
req.Response.Payload←2⊃2 ZipLevel Zipper sint req.Response.Payload
:Else
Log'ContentEncode: deflate content-encoding failed'
→End
:EndTrap
'Content-Encoding'req.SetHeader'deflate'
:Else
Log'ContentEncode: unsupported content-encoding - ',⊃enc ⍝ this should NEVER happen
:EndSelect
:EndIf
End:
:EndSection ⍝ Request Handling
∇ ip←GetIP objname
ip←{6::'' ⋄ ' (IP Address ',(⍕(_connections⍎⍵).IP),')'}objname
∇ r←CheckFunctionName fn
⍝ checks the requested function name and returns
⍝ 0 if the function is allowed
⍝ 404 (not found) either the function name does not exist, is not in IncludeFns (if defined), is in ExcludeFns (if defined)
:Access public
r←0
:If 1<|≡fn
r←CheckFunctionName¨fn
:Else
fn←⊆,fn
→0 If r←404×fn∊AppInitFn AppCloseFn ValidateRequestFn AuthenticateFn SessionInitFn
:If ~0∊_includeRegex
→0 If r←404×0∊(_includeRegex ⎕S'%')fn
:EndIf
:If ~0∊_excludeRegex
r←404×~0∊(_excludeRegex ⎕S'%')fn
:EndIf
:EndIf
:class Request
GetFromTable←{(⍵[;1]⍳⊂,)⊃⍵[;2],⊂''}
split←{p←(⍺⍷⍵)1 ⋄ ((p-1)↑⍵)(p↓⍵)} ⍝ Split ⍵ on first occurrence of
lc←0∘(819⌶)
deb←{{1↓¯1↓⍵/⍨~' '⍷⍵}' ',⍵,' '}
∇ {r}←{message}Fail status
⍝ Set HTTP response status code and message if status≠0
:Access public
:If r←0≠1↑status
:If 0=⎕NC'message'
:If 500=status
message←ErrorInfo
:Else
message←'' ⋄ :EndIf
:EndIf
message SetStatus status
:EndIf
∇ make
⍝ barebones constructor for interactive debugging (use Jarvis.MakeRequest '')
:Access public
:Implements constructor
makeResponse
∇ make1 args;query;origin;length;param;value;type
⍝ args is the result of Conga HTTPHeader event
:Access public
:Implements constructor
(Method Input HTTPVersion Headers)←args
Headers[;1]←lc Headers[;1] ⍝ header names are case insensitive
Method←lc Method
(ContentType param)←deb¨2↑(';'(≠⊆⊢)GetHeader'content-type'),⊂''
ContentType←lc ContentType
(type value)←2↑⊆deb¨'='(≠⊆⊢)param
:Select lc type
:Case '' ⍝ no parameter set
Charset←(ContentType≡'application/json')/'utf-8'
:Case 'charset'
Charset←lc value
:Case 'boundary'
Boundary←value
:EndSelect
Cookies←ParseCookies Headers
AcceptEncodings←ParseEncodings GetHeader'accept-encoding'
makeResponse
(Endpoint query)←'?'split Input
:Trap 11 ⍝ trap domain error on possible bad UTF-8 sequence
Endpoint←URLDecode Endpoint
QueryParams←ParseQueryString query
:If 'basic '≡lc 6↑auth←GetHeader'authorization'
(UserID Password)←':'split Base64Decode 6↓auth
:EndIf
:Else
Complete←1 ⍝ mark as complete
Fail 400 ⍝ 400 = bad request
→0
:EndTrap
length←GetHeader'content-length'
Complete←('get'≡Method)∧0=⊃⊃(//)⎕VFI length ⍝ we're a GET and there's no content-length or content-length=0
Complete←(0∊length)>/'chunked'⍷GetHeader'transfer-encoding' ⍝ or no length supplied and we're not chunked
∇ makeResponse
⍝ create the response namespace
Response←⎕NS''
Response.(Status StatusText Payload)←200 'OK' ''
Response.Headers←0 2'' ''
∇ ProcessBody args
:Access public
Body←args
Complete←1
∇ ProcessChunk args
:Access public
⍝ args is [1] chunk content [2] chunk-extension name/value pairs (which we don't expect and won't process)
Body,←1⊃args
∇ ProcessTrailer args;inds;mask
:Access public
args[;1]←lc args[;1]
mask←(≢Headers)≥inds←Headers[;1]args[;1]
Headers[mask/inds;2]←mask/args[;2]
Headers⍪←(~mask)⌿args
Complete←1
∇ r←Hostname;h
:Access public
:If ~0∊h←GetHeader'host'
r←'http',(~Server.Secure)↓'s://',h
:Else
r←Server.Hostname
:EndIf
∇ params←ParseQueryString query
params←0 2⊂''
→0⍨0∊query
:If '='∊query ⍝ contains name=value?
params←URLDecode¨2↑[2]↑'='(≠⊆⊢)¨'&'(≠⊆⊢)query
:Else
params←URLDecode query
:EndIf
∇ r←ParseEncodings encodings
r←(⎕C(⊃¨';'(≠⊆⊢)¨','(≠⊆⊢)encodings~' '))∩'gzip' 'deflate'
∇ cookies←ParseCookies headers;cookieHeader;cookie
:Access public shared
cookies←0 2⊂''
:For cookieHeader :In (headers[;1]≡¨⊂'cookie')/headers[;2]
:For cookie :In (({⍵↓⍨+/∧\' '=⍵}⌽)⍣2)¨';'(≠⊆⊢)cookieHeader
cookies⍪←2↑('='(≠⊆⊢)cookie),⊂''
:EndFor
:EndFor
cookies←(⌽≠⌽cookies[;1])⌿cookies
∇ r←URLDecode r;rgx;rgxu;i;j;z;t;m;⎕IO;lens;fill
:Access public shared
⍝ Decode a Percent Encoded string https://en.wikipedia.org/wiki/Percent-encoding
⎕IO←0
((r='+')/r)←' '
rgx←'[0-9a-fA-F]'
rgxu←'%[uU]',(4×rgx)rgx ⍝ 4 characters
r←(rgxu ⎕R{{⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳⍵}2↓⍵.Match})r
:If 0≠i←(r='%')/r
:AndIf 0≠i←(i≤¯2+r)/i
z←r[j←i∘.+1 2]
t←'UTF-8'⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'z
lens←⊃∘¨'UTF-8'∘⎕UCS¨t ⍝ UTF-8 is variable length encoding
fill←i[¯1↓+\0,lens]
r[fill]←t
m←(r)1 ⋄ m[(,j),i~fill]←0
r←m/r
:EndIf
base64←{⎕IO ⎕ML←0 1 ⍝ from dfns workspace - Base64 encoding and decoding as used in MIME.
bits←{,⍉(2)⊤⍵} ⍝ encode each element of ⍵ in bits, and catenate them all together
part←{((⍴⍵)↑1)⊂⍵} ⍝ partition ⍵ into chunks of length
0=2|⎕DR ⍵:2∘⊥∘(8∘↑)¨8 part{(-8|⍴⍵)↓⍵}6 bits{(⍵≠64)/⍵}chars⍵ ⍝ decode a string into octets
four←{ ⍝ use 4 characters to encode either
8=⍴⍵:'=='∇ ⍵,0 0 0 0 ⍝ 1,
16=⍴⍵:'='∇ ⍵,0 0 ⍝ 2
chars[2∘⊥¨6 part ⍵], ⍝ or 3 octets of input
}
cats←⊃∘(,/)∘((⊂'')∘,) ⍝ catenate zero or more strings
cats''∘four¨24 part 8 bits ⍵
}
∇ r←{cpo}Base64Encode w
⍝ Base64 Encode
⍝ Optional cpo (code points only) suppresses UTF-8 translation
⍝ if w is numeric (single byte integer), skip any conversion
:Access public shared
:If 83=⎕DR w ⋄ r←base64 w
:ElseIf 0=⎕NC'cpo' ⋄ r←base64'UTF-8'⎕UCS w
:Else ⋄ r←base64 ⎕UCS w
:EndIf
∇ r←{cpo}Base64Decode w
⍝ Base64 Decode
⍝ Optional cpo (code points only) suppresses UTF-8 translation
:Access public shared
:If 0=⎕NC'cpo' ⋄ r←'UTF-8'⎕UCS base64 w
:Else ⋄ r←⎕UCS base64 w
:EndIf
∇ r←{table}GetHeader name
:Access Public Instance
:If 0=⎕NC'table' ⋄ table←Headers ⋄ :EndIf
table[;1]←lc table[;1]
r←(lc name)GetFromTable table
∇ name DefaultHeader value
:Access public instance
:If 0∊Response.Headers GetHeader name
name SetHeader value
:EndIf
∇ r←{endpoint}MakeURI resource
:Access public instance
⍝ make a URI for a RESTful resource relative to the request endpoint
:If 0≠⎕NC'endpoint'
r←Hostname,endpoint,∊'/',¨⍕¨⊆resource
:Else
r←Hostname,Endpoint,∊'/',¨⍕¨⊆resource
:EndIf
∇ r←ErrorInfo
:Trap 0
r←⍕ErrorInfoLevel↑⎕DMX.(EM({⍵↑⍨⍵⍳']'}2⊃DM))
:Else
r←''
:EndTrap
∇ {(name value)}←name SetHeader value
:Access Public Instance
Response.Headers⍪←name(∊⍕value)
∇ value←GetCookie name
:Access public instance
⍝ retrieve a request cookie
value←(Cookies[;1]⍳⊆,name)⊃Cookies[;2],⊂''
∇ {status}←{statusText}SetStatus status
:Access public instance
:If status≠0
:If 0=⎕NC'statusText' ⋄ statusText←'' ⋄ :EndIf
statusText←{0∊⍵:⍵ ⋄ '('=⊣/⍵:⍵ ⋄ '(',⍵,')'}statusText
statusText←deb((HttpStatus[;1]status)⊃HttpStatus[;2],⊂''),' ',statusText
Response.(Status StatusText)←status statusText
:EndIf
∇ r←ContentTypeForFile filename;ext
:Access public instance
ext←⊂1↓3⊃⎕NPARTS filename
r←(ContentTypes[;1]ext)⊃ContentTypes[;2],⊂'text/html'
r,←('text/html'≡r)/'; charset=utf-8'
:EndClass
:Section SessionHandler
MakeSessionId←{⎕IO←0 ⋄((0(819⌶)⎕A),⎕A,⎕D)[(?2062),5↑1↓⎕TS]}
IsExpired←{≤0: 0 ⋄ (Now-⍵)>(×60000)÷86400000}
∇ r←DateToIDNX ts
⍝ Date to IDN eXtended (will be replaced by ⎕DT when ⎕DT is in the latest 3 versions of Dyalog APL)
r←(2 ⎕NQ'.' 'DateToIDN'(3↑ts))+(0 60 60 1000⊥¯4↑7↑ts)÷86400000
:EndSection
:Section Utilities
If←((0≠⊃)⊢)⍴⊣ ⍝ test for 0 return
isChar←{0 2∊⍨10|⎕DR ⍵}
toChar←{(⎕DR'')⎕DR ⍵}
stripQuotes←{'""'≡2↑¯1⌽⍵:¯1↓1↓⍵ ⋄ ⍵} ⍝ strip leading and ending "
deb←{{1↓¯1↓⍵/⍨~' '⍷⍵}' ',⍵,' '} ⍝ delete extraneous blanks
dlb←{⍵↓⍨+/∧\' '=⍵} ⍝ delete leading blanks
lc←0∘(819⌶) ⍝ lower case
uc←1∘(819⌶) ⍝ upper case
nameClass←{⎕NC⊂,'⍵'} ⍝ name class of argument
nocase←{(lc ) lc ⍵} ⍝ case insensitive operator
begins←{⍺≡()↑⍵} ⍝ does begin with ⍵?
ends←{⍺≡(-≢⍺)↑⍵} ⍝ does end with ⍵?
match←{ (≡nocase) ⍵} ⍝ case insensitive ≡
sins←{0∊:⍵ ⋄ } ⍝ set if not set
stopIf←{1∊⍵:-⎕TRAP←0 'C' '⎕←''Stopped for debugging... (Press Ctrl-Enter)''' ⋄ shy←0} ⍝ faster alternative to setting ⎕STOP
show←{(2⊃⎕SI),'[',(⍕2⊃⎕LC),'] ',⍵} ⍝ debugging utility
∇ r←DyalogRoot
r←{⍵,('/\'∊⍨⊢/⍵)↓'/'}{0∊t←2 ⎕NQ'.' 'GetEnvironment' 'DYALOG':⊃1 ⎕NPARTS⊃2 ⎕NQ'.' 'GetCommandLineArgs' ⋄ t}''
∇ r←MyAddr
:Access public shared
:Trap 0
r←2 ⎕NQ #'TCPGetHostID'
:Else
r←'localhost'
:EndTrap
∇ r←Now
r←DateToIDNX ⎕TS
∇ r←InTerm;system
:Access Public Shared
⍝ determine if interactive terminal is available
→0⍨r←~0∊2 ⎕NQ'.' 'GetEnvironment' 'RIDE_INIT'
→0⍨r←'Win' 'Dev'≡system←3↑¨(⊂1 4)⌷'.'⎕WG'APLVersion'
r←('Lin' 'Dev'≡system)∧{0::0 ⋄ 1⊣⎕SH'test -t 0'}''
∇ r←fmtTS ts
r←,'G⊂9999/99/99 @ 99:99:99⊃'⎕FMT 100⊥6↑ts
∇ r←a splitOn w
⍝ split a where w occurs (removing w from the result)
r←a{{(¯1+⊃¨⊆⍨⍵)↓¨⍵⊆⍺}(1+≢⍵)*⍵⍷⍺}w
∇ r←a splitOnFirst w
⍝ split a on first occurence of w (removing w from the result)
r←a{{(¯1+⊃¨⊆⍨⍵)↓¨⍵⊆⍺}(1+≢⍵)*<\⍵⍷⍺}w
∇ r←type ipRanges string;ranges
r←''
:Select ≢ranges←{('.'∊¨⍵){⊂1↓∊',',¨⍵}⌸⍵}string splitOn','
:Case 0
→0
:Case 1
r←,⊂((1+'.'∊⊃ranges)⊃'IPV6' 'IPV4')(⊃ranges)
:Case 2
r←↓'IPV4' 'IPV6',⍪ranges
:EndSelect
r←⊂(('Accept' 'Deny'⊂type)⊃'AllowEndPoints' 'DenyEndPoints')r
∇ r←isWin
⍝ are we running under Windows?
r←'Win'≡3↑⊃#.⎕WG'APLVersion'
∇ r←isRelPath w
⍝ is path w a relative path?
r←{{~'/\'∊⍨(⎕IO+2×isWin∧':'∊⍵)⊃⍵}3↑⍵}w
∇ r←isDir path
⍝ is path a directory?
r←{22::0 ⋄ 1=1 ⎕NINFO ⍵}path
∇ r←SourceFile;class
:If 0∊r←4⊃5179⌶class←⊃∊⎕CLASS ⎕THIS
r←{6::'' ⋄ ∊1 ⎕NPARTS ⍵⍎'SALT_Data.SourceFile'}class
:EndIf
∇ r←makeRegEx w
:Access public shared
⍝ convert a simple search using ? and * to regex
r←{0∊⍵:⍵
{'^',(⍵~'^$'),'$'}{¯1=⎕NC('A'@(∊∘'?*'))r←⍵:('/'=⊣/⍵)↓(¯1×'/'=⊢/⍵)↓⍵ ⍝ already regex? (remove leading/trailing '/'
r←∊(⊂'\.')@('.'=⊢)r ⍝ escape any periods
r←'.'@('?'=⊢)r ⍝ ? → .
r←∊(⊂'\/')@('/'=⊢)r ⍝ / → \/
∊(⊂'.*')@('*'=⊢)r ⍝ * → .*
}⍵ ⍝ add start and end of string markers
}w
∇ (rc msg)←{root}LoadFromFolder path;type;name;nsName;parts;ns;files;folders;file;folder;ref;r;m;findFiles;pattern
:Access public
⍝ Loads an APL "project" folder
(rc msg)←0 ''
root←{6::⍵ ⋄ root}#
findFiles←{
(names type hidden)←0 1 6(⎕NINFO⍠1)∊1 ⎕NPARTS path,'/',⍵
names/⍨(~hidden)∧type=2
}
files←''
:For pattern :In ','(≠⊆⊢)LoadableFiles
files,←findFiles pattern
:EndFor
folders←{
(names type hidden)←0 1 6(⎕NINFO⍠1)∊1 ⎕NPARTS path,'/*'
names/⍨(~hidden)∧type=1
}⍬
:For file :In files
:Trap 11
2(root ⍙FIX)'file://',file
:Else
msg,←'Unable to load file: ',file,⎕UCS 13
:EndTrap
:EndFor
:For folder :In folders
nsName←2⊃1 ⎕NPARTS folder
ref←0
:Select root.⎕NC⊂nsName
:Case 9.1 ⍝ namespace
ref←root⍎nsName
:Case 0 ⍝ not defined
ref←⍎nsName root.⎕NS''
:Else ⍝ oops
msg,←'"',folder,'" cannot be mapped to a valid namespace name',⎕UCS 13
:EndSelect
:If ref≢0
(r m)←ref LoadFromFolder folder
r←rc⌈r
msg,←m
:EndIf
:EndFor
msg←¯1↓msg
rc←4××≢msg
∇ {r}←{larg}(ref ⍙FIX)rarg;isArrayNotation;t;f;p
⍝ ⎕FIX cover that accommodates Array Notation and .apla files
⍝ revert to using ⎕FIX when it supports them
larg←{6::⍵ ⋄ larg}1
isArrayNotation←{~0 2∊⍨10|⎕DR ⍵:0 ⋄ {(⊃⍵)∊d←'[''¯.⊂⎕⍬',⎕D:1 ⋄ (2⊃2↑⍵)∊d,'( '}(∊⍵)~⎕UCS 9 32}
:Trap 0
:If 1=≡rarg
:AndIf 'file://'≡7↑rarg
:AndIf '.apla'≡lc⊃⌽p←⎕NPARTS f←7↓rarg
:If larg=2
r←ref⍎(2⊃p),'←',0 Deserialise⊃⎕NGET f
:Else
r←ref⍎0 Deserialise⊃⎕NGET f
:EndIf
:ElseIf isArrayNotation 1↓∊(⎕UCS 13),¨⊆rarg
r←ref⍎0 Deserialise rarg
:Else
r←larg ref.⎕FIX rarg
:EndIf
:Else
⎕SIGNAL⊂t,⍪⎕DMX⍎1⌽')(',∊⍕t←'EN' 'EM' 'Message'
:EndTrap
Deserialise←{
⍝ Convert text to array
⍺←⍬ ⍝ 1=execute expression; 0=return expression
⎕IO←0
Char←0 2∊⍨10|⎕DR
Num←2|⎕DR
Null←∧/⎕NULL≡¨⊢ ⍝ can't use ∧.= because = is pervasive on deep arrays
Ptr←6=10|⎕DR
Basic←CharNumNull
FirstNum←Num¨⊃⍤/⊢
FirstNs←{9∊⎕NC'⍵'}¨⊃⍤/⊢
sysVars←'⎕CT' '⎕DIV' '⎕IO' '⎕ML' '⎕PP' '⎕RL' '⎕RTL' '⎕WX' '⎕USING' '⎕AVU' '⎕DCT' '⎕FR'
L←lc
execute←FirstNum ,1
caller←FirstNs ,⊃⎕RSI
q←''''
SEP←'⋄',⎕UCS 10 13
Unquot←{( ⍵)×~≠\q=⍵}
SepMask←∊∘SEP Unquot
ParenLev←+\(ׯ3+7|¯3+'([{)]}'∘⍳)Unquot
Paren←1⌽')(',⊢
Split←{1↓¨⊂Over(1∘,)⍵}
Over←{(⍵⍵ )(⍵⍵ ⍵)}
EachIfAny←{0=≢⍵:⍵ ⋄ ⍺⍺¨⍵}
EachNonempty←{ EachIfAny Over((×≢¨⍵~¨' ')/⊢)⍵}
Parse←{
0=≢⍵:''
bot←0=
(2≤≢⍵)>/¯1↓bot: SubParse ⍵
p←bot×SepMask ⍵
/p:∊{1=≢⍵:',⊂',⍵ ⋄ ⍵}(Paren ∇)EachNonempty Over(p Split)⍵
p←2(1,>/¯1↓0,</)bot
/1↓p:∊(p⊂)∇¨p⊂⍵
}
ErrIfEmpty←{⍵⊣'Array doesn''t have a prototype'⎕SIGNAL 11/⍨(0=≢⍵)}
SubParse←{
('})]'⍳⊃⌽⍵)≠('{(['⍳⊃⍵):'Bad bracketing'⎕SIGNAL 2
(a w)←(1↓¯1∘↓)¨(-1)⍵
'['=⊃⍵:Paren'{⎕ML←1⋄↑⍵}1/¨',Paren ErrIfEmpty a Parse w ⍝ high-rank
':'∊⍵/⍨(1=)×~≠\q=⍵:a Namespace w ⍝ ns
'('=⊃⍵:Paren{⍵,'⎕NS⍬'/⍨0=≢⍵}a Parse w ⍝ vector/empty ns
⍵ ⍝ dfn
}
SysVar←(L sysVars)∊⍨' '~¨⍨L∘⊆
ParseLine←{
c←⍵':'
1≥≢(c↓⍵)~' ':'Missing value'⎕SIGNAL 6
name←c↑⍵
(SysVar⍱¯1≠⎕NC)name:'Invalid name'⎕SIGNAL 2
name(name,'←', Parse Over((c+1)↓⊢)⍵)
}
Namespace←{
p←(0=)×SepMask ⍵
(names assns)←↓⍉↑⍺ ParseLine EachNonempty Over(p Split)⍵
quadMask←SysVar names
quadAssns←'{⍵.(⍵',(∊'⊣',¨quadMask/assns),')}'
names/⍨←~quadMask
assns/⍨←~quadMask
∊'({'(assns,¨'⋄')quadAssns'⎕NS'('(, '∘,¨q,¨names,¨⊂q')')'}⍬)'
}
⍝ Make normalised simple vector:
w←↓⍣(2=≢⍴⍵)⊢⍵ ⍝ if mat, make nested
w←{¯1↓∊⍵,¨⎕UCS 13}⍣(2=|≡w)⊢w ⍝ if nested, make simple
w←'''[^'']*''' '⍝.*'⎕R'&' ''⊢w ⍝ strip comments
w/⍨←{(\⍵)∧⌽∨\⌽⍵}33≤⎕UCS w ⍝ strip leading/trailing non-printables
pl←ParenLev w
(0≠⊢/pl)(/0>pl):'Unmatched brackets'⎕SIGNAL 2
/(pl=0)×SepMask w:'Multi-line input'⎕SIGNAL 11
(⊃⎕RSI)Execute⍣execute⊢pl Parse w ⍝ materialise namespace as child of calling namespace
}
:EndSection
:Section HTML
∇ r←ScriptFollows
⍝ return the subsequent block of comments as a text script
r←{⍵/⍨'⍝'≠⊃¨⍵}{1↓¨⍵/⍨∧\'⍝'=⊃¨⍵}{⍵{((\⍵)∧⌽∨\⌽⍵)/}' '≠⍵}¨(1+2⊃⎕LC)↓↓(180⌶)2⊃⎕XSI
r←2↓∊(⎕UCS 13 10)∘,¨r
∇ r←{path}EndPoints ref;ns
:Access public
:If 0=⎕NC'path' ⋄ path←''
:Else ⋄ path,←'.'
:EndIf
r←path∘,¨{(⊂'')~⍨⍵.{⍵/⍨1 1 0≡×|⎕IO⊃⎕AT ⍵}¨⍵.⎕NL ¯3}ref ⍝ limit to result-returning monadic/dyadic/ambivalent functions
:For ns :In ref.⎕NL ¯9.1
r,←(path,ns)EndPoints ref⍎ns
:EndFor
∇ r←HtmlPage;endpoints
:Access public
r←ScriptFollows
endpoints←{⍵/⍨0=CheckFunctionName ⍵}EndPoints CodeLocation
:If 0∊endpoints
endpoints←'<b>No Endpoints Found</b>'
:Else
endpoints←∊{'<option value="',⍵,'">',⍵,'</option>'}¨'/'@('.'=⊢)¨endpoints
endpoints←'<select id="function" name="function">',endpoints,'</select>'
:EndIf
r←endpoints{i←⍵'⍠' ⋄ ((i-1)↑⍵),,i↓⍵}r
r←⎕UCS'UTF-8'⎕UCS r
:EndSection
:EndClass