langsmoke/ref_apl

2128 lines
84 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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