elm-ssr 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bin/elm-ssr.mjs +88 -4
- package/elm-src/ElmSsr/Db/Dsl.elm +4 -0
- package/elm-src/ElmSsr/Document/Events.elm +6 -1
- package/elm-src/ElmSsr/Form.elm +470 -0
- package/elm-src/ElmSsr/Html/Events.elm +4 -0
- package/elm-src/ElmSsr/Request/Decode.elm +95 -371
- package/lib/build.mjs +5 -0
- package/lib/query.mjs +42 -34
- package/lib/scaffold/app-templates.mjs +293 -0
- package/lib/scaffold/auth-add.mjs +305 -0
- package/lib/scaffold/auth-templates.mjs +915 -0
- package/lib/scaffold/codegen-bridge.mjs +122 -0
- package/lib/scaffold/route-template.mjs +132 -0
- package/lib/scaffold/runtime-template.mjs +237 -0
- package/lib/scaffold/strings.mjs +16 -0
- package/lib/scaffold/styles-template.mjs +305 -0
- package/lib/scaffold-codegen.mjs +3006 -0
- package/lib/scaffold.mjs +43 -1351
- package/package.json +1 -1
- package/src/client-runtime/islands.ts +10 -0
package/README.md
CHANGED
|
@@ -48,12 +48,13 @@ Creates a new app under `<workspace>/<name>/` (or `<workspace>/<subdir>/<name>/`
|
|
|
48
48
|
- `--db`: Configures SQLite database support and an initial migration.
|
|
49
49
|
- `--auth <betterAuth|auth0>`: Scaffolds authentication views, cookies, and callback handlers.
|
|
50
50
|
|
|
51
|
-
### `route <path> [--app <name>] [--api] [--ws] [--sse]`
|
|
52
|
-
Scaffolds a new route or endpoint:
|
|
51
|
+
### `route <path> [--app <name>] [--api] [--ws] [--sse] [--resource]`
|
|
52
|
+
Scaffolds a new route or endpoint (uses `ElmSsr.Form` for modern full-stack examples):
|
|
53
53
|
- `--app <app-name>`: Required if multiple apps are configured in the workspace.
|
|
54
54
|
- `--api`: Scaffolds a JSON API page/action route instead of an HTML page.
|
|
55
55
|
- `--ws` or `--websocket`: Scaffolds a TypeScript WebSocket handler in `src/Endpoints/<route>.ts`.
|
|
56
56
|
- `--sse`: Scaffolds a TypeScript Server-Sent Events (SSE) stream handler in `src/Endpoints/<route>.ts`.
|
|
57
|
+
- `--resource`: Generates a richer CRUD-style example with `Form` validation + Elmto hints.
|
|
57
58
|
|
|
58
59
|
### `query [--app <name>] [--dir <path>] [--output <path>]`
|
|
59
60
|
Generates type-safe Elm database schema and query helpers directly from raw SQL files in the migrations directory.
|
package/bin/elm-ssr.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import { createAppScaffold, createRouteScaffold } from "../lib/scaffold.mjs";
|
|
6
|
+
import { createAppScaffold, createRouteScaffold, addAuthProvider, listAuthProviders, ensureScaffoldCodegen } from "../lib/scaffold.mjs";
|
|
7
7
|
import { readWorkspaceConfig } from "../lib/workspace.mjs";
|
|
8
8
|
import { build } from "../lib/build.mjs";
|
|
9
9
|
import { migrate } from "../lib/migrate.mjs";
|
|
@@ -13,6 +13,12 @@ const defaultRootPath = process.cwd();
|
|
|
13
13
|
const args = process.argv.slice(2);
|
|
14
14
|
const command = args[0] ?? "help";
|
|
15
15
|
|
|
16
|
+
// Ensure Elm-based scaffold generator is available (hybrid Elm/JS for better debuggability).
|
|
17
|
+
// Also ensure on "build" so editing the Elm generator is reflected in dev/CI flows.
|
|
18
|
+
if (["new", "init", "route", "build"].includes(command)) {
|
|
19
|
+
await ensureScaffoldCodegen();
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
const ownPkg = JSON.parse(
|
|
17
23
|
await readFile(new URL("../package.json", import.meta.url), "utf8")
|
|
18
24
|
);
|
|
@@ -45,7 +51,7 @@ const userRoot = findFlagValue("--root");
|
|
|
45
51
|
let rootPath;
|
|
46
52
|
if (userRoot) {
|
|
47
53
|
rootPath = resolve(userRoot);
|
|
48
|
-
} else if (["new", "init", "migrate", "help"].includes(command)) {
|
|
54
|
+
} else if (["new", "init", "migrate", "help", "version", "--version", "-v"].includes(command)) {
|
|
49
55
|
rootPath = defaultRootPath;
|
|
50
56
|
} else {
|
|
51
57
|
rootPath = resolve(await findWorkspaceRoot(defaultRootPath));
|
|
@@ -91,6 +97,8 @@ const printHelp = () => {
|
|
|
91
97
|
info Print current workspace package and configured app names
|
|
92
98
|
migrate ... Apply / revert / inspect SQL migrations (see: elm-ssr migrate --help)
|
|
93
99
|
version Print the elm-ssr version
|
|
100
|
+
auth add <provider> [--app <name>] Add an auth provider to an app (betterAuth | auth0)
|
|
101
|
+
auth list [--app <name>] List configured auth providers
|
|
94
102
|
`);
|
|
95
103
|
};
|
|
96
104
|
|
|
@@ -276,7 +284,8 @@ switch (command) {
|
|
|
276
284
|
requireConfig();
|
|
277
285
|
const routePath = args[1];
|
|
278
286
|
if (!routePath) {
|
|
279
|
-
console.error("Usage: elm-ssr route <path> [--app <app-name>] [--api] [--ws] [--sse]");
|
|
287
|
+
console.error("Usage: elm-ssr route <path> [--app <app-name>] [--api] [--ws] [--sse] [--resource]");
|
|
288
|
+
console.error(" --resource Scaffold a richer CRUD-ish page using Form (recommended for full-stack examples)");
|
|
280
289
|
process.exit(1);
|
|
281
290
|
}
|
|
282
291
|
|
|
@@ -301,8 +310,9 @@ switch (command) {
|
|
|
301
310
|
const isApi = args.includes("--api");
|
|
302
311
|
const isWs = args.includes("--ws") || args.includes("--websocket");
|
|
303
312
|
const isSse = args.includes("--sse");
|
|
313
|
+
const isResource = args.includes("--resource");
|
|
304
314
|
|
|
305
|
-
const result = await createRouteScaffold(rootPath, appConfig, routePath, { isApi, isWs, isSse });
|
|
315
|
+
const result = await createRouteScaffold(rootPath, appConfig, routePath, { isApi, isWs, isSse, isResource });
|
|
306
316
|
console.log(`Scaffolded ${result.type} route at ${result.path}`);
|
|
307
317
|
if (result.instructions) {
|
|
308
318
|
console.log("\nInstructions to wire it up:\n" + result.instructions);
|
|
@@ -348,6 +358,80 @@ switch (command) {
|
|
|
348
358
|
break;
|
|
349
359
|
}
|
|
350
360
|
|
|
361
|
+
case "auth": {
|
|
362
|
+
const subcommand = args[1];
|
|
363
|
+
|
|
364
|
+
if (subcommand === "list") {
|
|
365
|
+
requireConfig();
|
|
366
|
+
const appName = findFlagValue("--app");
|
|
367
|
+
const apps = appName
|
|
368
|
+
? config.apps.filter(a => a.name === appName)
|
|
369
|
+
: config.apps;
|
|
370
|
+
if (apps.length === 0) {
|
|
371
|
+
console.error(appName
|
|
372
|
+
? `Error: App "${appName}" not found in elm-ssr.config.json`
|
|
373
|
+
: "Error: No apps found in elm-ssr.config.json");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
for (const app of apps) {
|
|
377
|
+
const providers = await listAuthProviders(rootPath, app);
|
|
378
|
+
if (providers.length === 0) {
|
|
379
|
+
console.log(`${app.name}: no auth providers configured`);
|
|
380
|
+
} else {
|
|
381
|
+
console.log(`${app.name}: ${providers.join(", ")}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (subcommand === "add") {
|
|
388
|
+
requireConfig();
|
|
389
|
+
const providerArg = args[2];
|
|
390
|
+
if (!providerArg) {
|
|
391
|
+
console.error("Usage: elm-ssr auth add <provider> [--app <name>]");
|
|
392
|
+
console.error(" Providers: betterAuth, auth0");
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
const appName = findFlagValue("--app");
|
|
396
|
+
let appConfig;
|
|
397
|
+
if (appName) {
|
|
398
|
+
appConfig = config.apps.find(a => a.name === appName);
|
|
399
|
+
if (!appConfig) {
|
|
400
|
+
console.error(`Error: App "${appName}" not found in elm-ssr.config.json`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
} else if (config.apps.length === 1) {
|
|
404
|
+
appConfig = config.apps[0];
|
|
405
|
+
} else {
|
|
406
|
+
console.error("Error: Multiple apps found. Specify one with --app <name>");
|
|
407
|
+
console.error("Available:", config.apps.map(a => a.name).join(", "));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const result = await addAuthProvider(rootPath, appConfig, providerArg);
|
|
412
|
+
console.log(`Added ${result.name} to ${appConfig.name}`);
|
|
413
|
+
console.log(`\nNext steps:`);
|
|
414
|
+
if (result.provider === "better-auth") {
|
|
415
|
+
console.log(` 1. Run: bun install`);
|
|
416
|
+
console.log(` 2. Run: elm-ssr migrate`);
|
|
417
|
+
console.log(` 3. Set BETTER_AUTH_SECRET in ${appConfig.root}/.dev.vars`);
|
|
418
|
+
} else {
|
|
419
|
+
console.log(` 1. Set AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET in ${appConfig.root}/.dev.vars`);
|
|
420
|
+
}
|
|
421
|
+
console.log(` 4. Run: bun run build`);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error(`Error: ${err.message}`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.error("Usage: elm-ssr auth <add|list> [...]");
|
|
430
|
+
console.error(" elm-ssr auth add betterAuth --app my-app");
|
|
431
|
+
console.error(" elm-ssr auth list --app my-app");
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
351
435
|
case "migrate":
|
|
352
436
|
await migrate(args.slice(1));
|
|
353
437
|
break;
|
|
@@ -13,6 +13,10 @@ module ElmSsr.Db.Dsl exposing
|
|
|
13
13
|
, compileQuery
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
{-| LEGACY. Elmto is canonical (ElmSsr.Db.Elmto + Repo + Query).
|
|
17
|
+
Generator no longer produces Dsl modules. This is for porting only.
|
|
18
|
+
-}
|
|
19
|
+
|
|
16
20
|
import Json.Decode as Decode exposing (Decoder)
|
|
17
21
|
import Json.Encode as Encode
|
|
18
22
|
import ElmSsr.Loader as Loader exposing (Loader)
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
module ElmSsr.Document.Events exposing (EventRef, decodeEventRef, encodeEventRef, findMessage)
|
|
2
2
|
|
|
3
|
-
{-|
|
|
3
|
+
{-| LEGACY / vestigial.
|
|
4
|
+
|
|
5
|
+
Only used to support very old page event wiring if any; islands should use
|
|
6
|
+
stock `elm/html` `Html.Events` + the client runtime.
|
|
7
|
+
|
|
8
|
+
Browser events are bridged without serializing `Msg`. An event handler is
|
|
4
9
|
rendered as an `EventRef` (a DOM path + event name); when the browser reports an
|
|
5
10
|
event, the runtime looks the message back up against the current view with
|
|
6
11
|
[`findMessage`](#findMessage).
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
module ElmSsr.Form exposing
|
|
2
|
+
( Decoder, Error, FieldDecoder
|
|
3
|
+
, required, optional, optionalWithDefault
|
|
4
|
+
, string, int, float, bool
|
|
5
|
+
, validate, custom
|
|
6
|
+
, succeed, fail, map, map2, map3, map4, map5, map6, map7, map8, andThen
|
|
7
|
+
, decode
|
|
8
|
+
, email, nonEmpty, minInt, maxInt, minFloat, maxFloat, minLength, maxLength
|
|
9
|
+
, errorFor, hasError, errorsFor
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
{-| Type-safe form and request validation.
|
|
13
|
+
|
|
14
|
+
Decoders are pure and can be used on the server (via Request data) and in islands
|
|
15
|
+
(via plain key/value pairs from model state). Errors accumulate.
|
|
16
|
+
|
|
17
|
+
Use on the server with `decode` + `Request.formData` (or the thin wrappers in
|
|
18
|
+
`ElmSsr.Request.Decode`).
|
|
19
|
+
|
|
20
|
+
# Core types
|
|
21
|
+
@docs Decoder, Error
|
|
22
|
+
|
|
23
|
+
# Building decoders
|
|
24
|
+
@docs required, optional, optionalWithDefault
|
|
25
|
+
@docs string, int, float, bool
|
|
26
|
+
@docs validate, custom
|
|
27
|
+
@docs succeed, fail, map, map2, map3, map4, map5, map6, map7, map8, andThen
|
|
28
|
+
|
|
29
|
+
# Running
|
|
30
|
+
@docs decode
|
|
31
|
+
|
|
32
|
+
# Validators
|
|
33
|
+
@docs email, nonEmpty, minInt, maxInt, minFloat, maxFloat, minLength, maxLength
|
|
34
|
+
|
|
35
|
+
# Error helpers (usable on server and client)
|
|
36
|
+
@docs errorFor, hasError, errorsFor
|
|
37
|
+
-}
|
|
38
|
+
|
|
39
|
+
import Json.Decode as JD
|
|
40
|
+
import Json.Encode as JE
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
type alias Error =
|
|
44
|
+
{ field : String
|
|
45
|
+
, message : String
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
type Decoder a
|
|
50
|
+
= Decoder (List ( String, String ) -> Result (List Error) a)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
type FieldDecoder a
|
|
54
|
+
= FieldDecoder (Maybe String -> Result String a)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
decode : Decoder a -> List ( String, String ) -> Result (List Error) a
|
|
58
|
+
decode (Decoder dec) input =
|
|
59
|
+
dec input
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
decodeField : FieldDecoder a -> Maybe String -> Result Error a
|
|
63
|
+
decodeField (FieldDecoder fd) raw =
|
|
64
|
+
case fd raw of
|
|
65
|
+
Ok v ->
|
|
66
|
+
Ok v
|
|
67
|
+
|
|
68
|
+
Err msg ->
|
|
69
|
+
Err { field = "", message = msg }
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
required : String -> FieldDecoder b -> Decoder (b -> a) -> Decoder a
|
|
73
|
+
required name (FieldDecoder fieldDec) (Decoder decFn) =
|
|
74
|
+
Decoder (\input ->
|
|
75
|
+
let
|
|
76
|
+
rawValue =
|
|
77
|
+
lookup name input
|
|
78
|
+
|
|
79
|
+
resFn =
|
|
80
|
+
decFn input
|
|
81
|
+
|
|
82
|
+
resVal =
|
|
83
|
+
case fieldDec rawValue of
|
|
84
|
+
Ok val ->
|
|
85
|
+
Ok val
|
|
86
|
+
|
|
87
|
+
Err msg ->
|
|
88
|
+
Err [ { field = name, message = msg } ]
|
|
89
|
+
in
|
|
90
|
+
combine resFn resVal
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
optional : String -> FieldDecoder b -> Decoder (Maybe b -> a) -> Decoder a
|
|
95
|
+
optional name (FieldDecoder fieldDec) (Decoder decFn) =
|
|
96
|
+
Decoder (\input ->
|
|
97
|
+
let
|
|
98
|
+
rawValue =
|
|
99
|
+
lookup name input
|
|
100
|
+
|
|
101
|
+
resFn =
|
|
102
|
+
decFn input
|
|
103
|
+
|
|
104
|
+
resVal =
|
|
105
|
+
case rawValue of
|
|
106
|
+
Nothing ->
|
|
107
|
+
Ok Nothing
|
|
108
|
+
|
|
109
|
+
Just "" ->
|
|
110
|
+
Ok Nothing
|
|
111
|
+
|
|
112
|
+
Just val ->
|
|
113
|
+
case fieldDec (Just val) of
|
|
114
|
+
Ok decoded ->
|
|
115
|
+
Ok (Just decoded)
|
|
116
|
+
|
|
117
|
+
Err msg ->
|
|
118
|
+
Err [ { field = name, message = msg } ]
|
|
119
|
+
in
|
|
120
|
+
combine resFn resVal
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
optionalWithDefault : String -> b -> FieldDecoder b -> Decoder (b -> a) -> Decoder a
|
|
125
|
+
optionalWithDefault name defaultVal (FieldDecoder fieldDec) (Decoder decFn) =
|
|
126
|
+
Decoder (\input ->
|
|
127
|
+
let
|
|
128
|
+
rawValue =
|
|
129
|
+
lookup name input
|
|
130
|
+
|
|
131
|
+
resFn =
|
|
132
|
+
decFn input
|
|
133
|
+
|
|
134
|
+
resVal =
|
|
135
|
+
case rawValue of
|
|
136
|
+
Nothing ->
|
|
137
|
+
Ok defaultVal
|
|
138
|
+
|
|
139
|
+
Just "" ->
|
|
140
|
+
Ok defaultVal
|
|
141
|
+
|
|
142
|
+
Just val ->
|
|
143
|
+
case fieldDec (Just val) of
|
|
144
|
+
Ok decoded ->
|
|
145
|
+
Ok decoded
|
|
146
|
+
|
|
147
|
+
Err msg ->
|
|
148
|
+
Err [ { field = name, message = msg } ]
|
|
149
|
+
in
|
|
150
|
+
combine resFn resVal
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
string : FieldDecoder String
|
|
155
|
+
string =
|
|
156
|
+
FieldDecoder (\rawValue ->
|
|
157
|
+
case rawValue of
|
|
158
|
+
Just val ->
|
|
159
|
+
Ok val
|
|
160
|
+
|
|
161
|
+
Nothing ->
|
|
162
|
+
Err "Field is required"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
int : FieldDecoder Int
|
|
167
|
+
int =
|
|
168
|
+
FieldDecoder (\rawValue ->
|
|
169
|
+
case rawValue of
|
|
170
|
+
Just val ->
|
|
171
|
+
case String.toInt val of
|
|
172
|
+
Just num ->
|
|
173
|
+
Ok num
|
|
174
|
+
|
|
175
|
+
Nothing ->
|
|
176
|
+
Err "Must be a valid integer"
|
|
177
|
+
|
|
178
|
+
Nothing ->
|
|
179
|
+
Err "Field is required"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
float : FieldDecoder Float
|
|
184
|
+
float =
|
|
185
|
+
FieldDecoder (\rawValue ->
|
|
186
|
+
case rawValue of
|
|
187
|
+
Just val ->
|
|
188
|
+
case String.toFloat val of
|
|
189
|
+
Just num ->
|
|
190
|
+
Ok num
|
|
191
|
+
|
|
192
|
+
Nothing ->
|
|
193
|
+
Err "Must be a valid number"
|
|
194
|
+
|
|
195
|
+
Nothing ->
|
|
196
|
+
Err "Field is required"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
bool : FieldDecoder Bool
|
|
201
|
+
bool =
|
|
202
|
+
FieldDecoder (\rawValue ->
|
|
203
|
+
case rawValue of
|
|
204
|
+
Just "true" ->
|
|
205
|
+
Ok True
|
|
206
|
+
|
|
207
|
+
Just "false" ->
|
|
208
|
+
Ok False
|
|
209
|
+
|
|
210
|
+
Just "on" ->
|
|
211
|
+
Ok True
|
|
212
|
+
|
|
213
|
+
Just "" ->
|
|
214
|
+
Ok False
|
|
215
|
+
|
|
216
|
+
Just _ ->
|
|
217
|
+
Ok False
|
|
218
|
+
|
|
219
|
+
Nothing ->
|
|
220
|
+
Ok False
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
validate : (a -> Result String a) -> FieldDecoder a -> FieldDecoder a
|
|
225
|
+
validate validator (FieldDecoder fieldDec) =
|
|
226
|
+
FieldDecoder (\rawValue ->
|
|
227
|
+
fieldDec rawValue
|
|
228
|
+
|> Result.andThen validator
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
custom : (a -> Result String b) -> FieldDecoder a -> FieldDecoder b
|
|
233
|
+
custom f (FieldDecoder fieldDec) =
|
|
234
|
+
FieldDecoder (\rawValue ->
|
|
235
|
+
fieldDec rawValue
|
|
236
|
+
|> Result.andThen f
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
succeed : a -> Decoder a
|
|
241
|
+
succeed val =
|
|
242
|
+
Decoder (\_ -> Ok val)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
fail : String -> String -> Decoder a
|
|
246
|
+
fail fieldName msg =
|
|
247
|
+
Decoder (\_ -> Err [ { field = fieldName, message = msg } ])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
map : (a -> b) -> Decoder a -> Decoder b
|
|
251
|
+
map f (Decoder dec) =
|
|
252
|
+
Decoder (\input -> Result.map f (dec input))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
apply : Decoder a -> Decoder (a -> b) -> Decoder b
|
|
256
|
+
apply (Decoder decVal) (Decoder decFn) =
|
|
257
|
+
Decoder (\input ->
|
|
258
|
+
combine (decFn input) (decVal input)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c
|
|
263
|
+
map2 f decA decB =
|
|
264
|
+
succeed f
|
|
265
|
+
|> apply decA
|
|
266
|
+
|> apply decB
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
map3 : (a -> b -> c -> d) -> Decoder a -> Decoder b -> Decoder c -> Decoder d
|
|
270
|
+
map3 f decA decB decC =
|
|
271
|
+
succeed f
|
|
272
|
+
|> apply decA
|
|
273
|
+
|> apply decB
|
|
274
|
+
|> apply decC
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
map4 : (a -> b -> c -> d -> e) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e
|
|
278
|
+
map4 f decA decB decC decD =
|
|
279
|
+
succeed f
|
|
280
|
+
|> apply decA
|
|
281
|
+
|> apply decB
|
|
282
|
+
|> apply decC
|
|
283
|
+
|> apply decD
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
map5 : (a -> b -> c -> d -> e -> f) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f
|
|
287
|
+
map5 f decA decB decC decD decE =
|
|
288
|
+
succeed f
|
|
289
|
+
|> apply decA
|
|
290
|
+
|> apply decB
|
|
291
|
+
|> apply decC
|
|
292
|
+
|> apply decD
|
|
293
|
+
|> apply decE
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
map6 : (a -> b -> c -> d -> e -> f -> g) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g
|
|
297
|
+
map6 f decA decB decC decD decE decF =
|
|
298
|
+
succeed f
|
|
299
|
+
|> apply decA
|
|
300
|
+
|> apply decB
|
|
301
|
+
|> apply decC
|
|
302
|
+
|> apply decD
|
|
303
|
+
|> apply decE
|
|
304
|
+
|> apply decF
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
map7 : (a -> b -> c -> d -> e -> f -> g -> h) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h
|
|
308
|
+
map7 f decA decB decC decD decE decF decG =
|
|
309
|
+
succeed f
|
|
310
|
+
|> apply decA
|
|
311
|
+
|> apply decB
|
|
312
|
+
|> apply decC
|
|
313
|
+
|> apply decD
|
|
314
|
+
|> apply decE
|
|
315
|
+
|> apply decF
|
|
316
|
+
|> apply decG
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
map8 : (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h -> Decoder i
|
|
320
|
+
map8 f decA decB decC decD decE decF decG decH =
|
|
321
|
+
succeed f
|
|
322
|
+
|> apply decA
|
|
323
|
+
|> apply decB
|
|
324
|
+
|> apply decC
|
|
325
|
+
|> apply decD
|
|
326
|
+
|> apply decE
|
|
327
|
+
|> apply decF
|
|
328
|
+
|> apply decG
|
|
329
|
+
|> apply decH
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
andThen : (a -> Decoder b) -> Decoder a -> Decoder b
|
|
333
|
+
andThen f (Decoder decA) =
|
|
334
|
+
Decoder (\input ->
|
|
335
|
+
case decA input of
|
|
336
|
+
Ok a ->
|
|
337
|
+
let
|
|
338
|
+
(Decoder decB) =
|
|
339
|
+
f a
|
|
340
|
+
in
|
|
341
|
+
decB input
|
|
342
|
+
|
|
343
|
+
Err errs ->
|
|
344
|
+
Err errs
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
email : String -> Result String String
|
|
349
|
+
email val =
|
|
350
|
+
let
|
|
351
|
+
parts =
|
|
352
|
+
String.split "@" val
|
|
353
|
+
in
|
|
354
|
+
case parts of
|
|
355
|
+
[ _, domain ] ->
|
|
356
|
+
if String.contains "." domain then
|
|
357
|
+
Ok val
|
|
358
|
+
|
|
359
|
+
else
|
|
360
|
+
Err "Invalid email address"
|
|
361
|
+
|
|
362
|
+
_ ->
|
|
363
|
+
Err "Invalid email address"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
nonEmpty : String -> Result String String
|
|
367
|
+
nonEmpty val =
|
|
368
|
+
if String.isEmpty (String.trim val) then
|
|
369
|
+
Err "Cannot be empty"
|
|
370
|
+
|
|
371
|
+
else
|
|
372
|
+
Ok val
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
minInt : Int -> Int -> Result String Int
|
|
376
|
+
minInt min val =
|
|
377
|
+
if val >= min then
|
|
378
|
+
Ok val
|
|
379
|
+
|
|
380
|
+
else
|
|
381
|
+
Err ("Must be at least " ++ String.fromInt min)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
maxInt : Int -> Int -> Result String Int
|
|
385
|
+
maxInt max val =
|
|
386
|
+
if val <= max then
|
|
387
|
+
Ok val
|
|
388
|
+
|
|
389
|
+
else
|
|
390
|
+
Err ("Must be at most " ++ String.fromInt max)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
minFloat : Float -> Float -> Result String Float
|
|
394
|
+
minFloat min val =
|
|
395
|
+
if val >= min then
|
|
396
|
+
Ok val
|
|
397
|
+
|
|
398
|
+
else
|
|
399
|
+
Err ("Must be at least " ++ String.fromFloat min)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
maxFloat : Float -> Float -> Result String Float
|
|
403
|
+
maxFloat max val =
|
|
404
|
+
if val <= max then
|
|
405
|
+
Ok val
|
|
406
|
+
|
|
407
|
+
else
|
|
408
|
+
Err ("Must be at most " ++ String.fromFloat max)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
minLength : Int -> String -> Result String String
|
|
412
|
+
minLength len val =
|
|
413
|
+
if String.length val >= len then
|
|
414
|
+
Ok val
|
|
415
|
+
|
|
416
|
+
else
|
|
417
|
+
Err ("Must be at least " ++ String.fromInt len ++ " characters")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
maxLength : Int -> String -> Result String String
|
|
421
|
+
maxLength len val =
|
|
422
|
+
if String.length val <= len then
|
|
423
|
+
Ok val
|
|
424
|
+
|
|
425
|
+
else
|
|
426
|
+
Err ("Must be at most " ++ String.fromInt len ++ " characters")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
errorFor : String -> List Error -> Maybe String
|
|
430
|
+
errorFor field errors =
|
|
431
|
+
errors
|
|
432
|
+
|> List.filter (\e -> e.field == field)
|
|
433
|
+
|> List.head
|
|
434
|
+
|> Maybe.map .message
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
hasError : String -> List Error -> Bool
|
|
438
|
+
hasError field errors =
|
|
439
|
+
List.any (\e -> e.field == field) errors
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
errorsFor : String -> List Error -> List String
|
|
443
|
+
errorsFor field errors =
|
|
444
|
+
errors
|
|
445
|
+
|> List.filter (\e -> e.field == field)
|
|
446
|
+
|> List.map .message
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
lookup : String -> List ( String, String ) -> Maybe String
|
|
450
|
+
lookup key pairs =
|
|
451
|
+
pairs
|
|
452
|
+
|> List.filter (\( name, _ ) -> name == key)
|
|
453
|
+
|> List.head
|
|
454
|
+
|> Maybe.map Tuple.second
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
combine : Result (List Error) (a -> b) -> Result (List Error) a -> Result (List Error) b
|
|
458
|
+
combine resFn resVal =
|
|
459
|
+
case ( resFn, resVal ) of
|
|
460
|
+
( Ok fn, Ok val ) ->
|
|
461
|
+
Ok (fn val)
|
|
462
|
+
|
|
463
|
+
( Err errsFn, Err errsVal ) ->
|
|
464
|
+
Err (errsFn ++ errsVal)
|
|
465
|
+
|
|
466
|
+
( Err errs, Ok _ ) ->
|
|
467
|
+
Err errs
|
|
468
|
+
|
|
469
|
+
( Ok _, Err errs ) ->
|
|
470
|
+
Err errs
|
|
@@ -5,6 +5,10 @@ module ElmSsr.Html.Events exposing
|
|
|
5
5
|
, onBlur, onFocus
|
|
6
6
|
)
|
|
7
7
|
|
|
8
|
+
{-| LEGACY / vestigial. Pages are static SSR; events belong in islands using stock elm/html Html.Events.
|
|
9
|
+
Kept only if Document.Events still references for legacy findMessage.
|
|
10
|
+
-}
|
|
11
|
+
|
|
8
12
|
{-| Event handlers. Mirrors `elm/html`'s `Html.Events`.
|
|
9
13
|
|
|
10
14
|
Handlers are bridged by DOM path and event name, not by serializing `Msg`, so
|