Skip to content

Commit dd27d65

Browse files
committed
refactor: drop createLanguageThis
Migrate all languages to createLanguage and update bench for string extract. Bump to 0.0.15.
1 parent f5635d4 commit dd27d65

19 files changed

Lines changed: 1121 additions & 1188 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Online playground: https://claudiuceia.github.io/ts-duckling/
2222
## Install (Deno / JSR)
2323

2424
```ts
25-
import { Duckling } from "jsr:@claudiu-ceia/ts-duckling@^0.0.14";
25+
import { Duckling } from "jsr:@claudiu-ceia/ts-duckling@^0.0.15";
2626
```
2727

2828
## Quickstart
@@ -33,7 +33,7 @@ import {
3333
Email,
3434
Time,
3535
URL,
36-
} from "jsr:@claudiu-ceia/ts-duckling@^0.0.14";
36+
} from "jsr:@claudiu-ceia/ts-duckling@^0.0.15";
3737

3838
const text =
3939
"Email me at foo@example.com and visit https://example.com at 2004-07-12T22:18:09Z.";
@@ -147,7 +147,7 @@ import {
147147
Duckling,
148148
ent,
149149
type Entity,
150-
} from "jsr:@claudiu-ceia/ts-duckling@^0.0.14";
150+
} from "jsr:@claudiu-ceia/ts-duckling@^0.0.15";
151151

152152
type HashtagEntity = Entity<"hashtag", { tag: string }>;
153153

bench/extract.bench.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ const textPII = [
3131
].join(" | ");
3232

3333
Deno.bench("extract: PII-heavy (many matches)", () => {
34-
const res = piiDuckling.extract({ text: textPII, index: 0 });
35-
if (!res.success) throw new Error("unexpected parse failure");
36-
if (res.value.length < 6) throw new Error("unexpected low match count");
34+
const entities = piiDuckling.extract(textPII);
35+
if (entities.length < 6) throw new Error("unexpected low match count");
3736
});
3837

3938
const defaultDuckling = Duckling();
@@ -46,9 +45,8 @@ const textMixed = [
4645

4746
Deno.bench("extract: default Duckling (mixed)", () => {
4847
using _time = new FakeTime("2022-01-07T12:00:00.000Z");
49-
const res = defaultDuckling.extract({ text: textMixed, index: 0 });
50-
if (!res.success) throw new Error("unexpected parse failure");
51-
if (!res.value.some((e) => e.kind === "time")) {
48+
const entities = defaultDuckling.extract(textMixed);
49+
if (!entities.some((e) => e.kind === "time")) {
5250
throw new Error("expected at least one time entity");
5351
}
5452
});

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@claudiu-ceia/ts-duckling",
3-
"version": "0.0.14",
3+
"version": "0.0.15",
44
"exports": {
55
".": "./mod.ts"
66
},

mod.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
any,
33
anyChar,
4-
createLanguageThis,
4+
createLanguage,
55
eof,
66
failure,
77
manyTill,
@@ -56,9 +56,9 @@ export type AnyEntity =
5656
| ApiKeyEntity;
5757

5858
type DucklingLanguage<E extends Entity<string, unknown>> = {
59-
Entity: () => Parser<E[]>;
60-
Unstructured: () => Parser<string>;
61-
extract: () => Parser<E[]>;
59+
Entity: Parser<E[]>;
60+
Unstructured: Parser<string>;
61+
extract: Parser<E[]>;
6262
};
6363

6464
/**
@@ -123,8 +123,8 @@ export function Duckling<
123123
type E = EntityFromParsers<P> | AnyEntity;
124124
const ps = (parsers ?? DefaultParsers) as unknown as readonly Parser<E>[];
125125

126-
const lang = createLanguageThis<DucklingLanguage<E>>({
127-
Entity() {
126+
const lang: DucklingLanguage<E> = createLanguage<DucklingLanguage<E>>({
127+
Entity: () => {
128128
if (ps.length === 0) {
129129
return (ctx) => failure(ctx, "entity");
130130
}
@@ -140,16 +140,16 @@ export function Duckling<
140140
);
141141
return map(p, (recs) => recs.map((r) => r.value));
142142
},
143-
Unstructured() {
143+
Unstructured: () => {
144144
return any(dot(word), __(word), space());
145145
},
146-
extract() {
146+
extract: (s) => {
147147
return map(
148148
seq(
149149
optional(space()),
150150
map(
151151
manyTill(
152-
any(this.Entity, skip1(this.Unstructured), skip1(anyChar())),
152+
any(s.Entity, skip1(s.Unstructured), skip1(anyChar())),
153153
skip1(eof()),
154154
),
155155
([...matches]) =>

src/ApiKey.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
any,
33
type Context,
4-
createLanguageThis,
4+
createLanguage,
55
map,
66
regex,
77
seq,
@@ -24,10 +24,10 @@ export type ApiKeyEntity = Entity<
2424
>;
2525

2626
type ApiKeyLanguage = {
27-
Prefix: () => Parser<string>;
28-
PrefixKey: () => Parser<ApiKeyEntity["value"]>;
29-
Full: () => Parser<ApiKeyEntity>;
30-
parser: () => Parser<ApiKeyEntity>;
27+
Prefix: Parser<string>;
28+
PrefixKey: Parser<ApiKeyEntity["value"]>;
29+
Full: Parser<ApiKeyEntity>;
30+
parser: Parser<ApiKeyEntity>;
3131
};
3232

3333
const ProviderPrefixes: Record<string, string> = {
@@ -114,40 +114,39 @@ export const apiKey = (
114114
/**
115115
* API key parser language.
116116
*/
117-
export const ApiKey: ReturnType<typeof createLanguageThis<ApiKeyLanguage>> =
118-
createLanguageThis<ApiKeyLanguage>({
119-
/**
120-
* Matches prefix for common API key formats, e.g. "sk-" for Stripe, "pk-" for some others, etc.
121-
* This is optional since not all API keys have a prefix.
122-
*
123-
* Returns a provider name if a known prefix is matched, or undefined otherwise.
124-
* This allows downstream code to potentially apply provider-specific validation or parsing logic.
125-
*/
126-
Prefix(): Parser<string> {
127-
return map(
117+
export const ApiKey: ApiKeyLanguage = createLanguage<ApiKeyLanguage>({
118+
/**
119+
* Matches prefix for common API key formats, e.g. "sk-" for Stripe, "pk-" for some others, etc.
120+
* This is optional since not all API keys have a prefix.
121+
*
122+
* Returns a provider name if a known prefix is matched, or undefined otherwise.
123+
* This allows downstream code to potentially apply provider-specific validation or parsing logic.
124+
*/
125+
Prefix: (): Parser<string> => {
126+
return map(
127+
regex(ProviderPrefixRegex, "api-key-prefix"),
128+
(prefix) => ProviderPrefixes[prefix],
129+
);
130+
},
131+
/**
132+
* Parses a known provider prefix + key body.
133+
*/
134+
PrefixKey: (): Parser<ApiKeyEntity["value"]> => {
135+
return map(
136+
seq(
128137
regex(ProviderPrefixRegex, "api-key-prefix"),
129-
(prefix) => ProviderPrefixes[prefix],
130-
);
131-
},
132-
/**
133-
* Parses a known provider prefix + key body.
134-
*/
135-
PrefixKey(): Parser<ApiKeyEntity["value"]> {
136-
return map(
137-
seq(
138-
regex(ProviderPrefixRegex, "api-key-prefix"),
139-
regex(/[A-Za-z0-9][A-Za-z0-9._-]{7,199}/, "api-key-body"),
140-
),
141-
([prefix, body]) => ({
142-
provider: ProviderPrefixes[prefix],
143-
key: `${prefix}${body}`,
144-
}),
145-
);
146-
},
147-
Full(): Parser<ApiKeyEntity> {
148-
return map(this.PrefixKey, (value, b, a) => apiKey(value, b, a));
149-
},
150-
parser(): Parser<ApiKeyEntity> {
151-
return dot(any(this.Full));
152-
},
153-
});
138+
regex(/[A-Za-z0-9][A-Za-z0-9._-]{7,199}/, "api-key-body"),
139+
),
140+
([prefix, body]) => ({
141+
provider: ProviderPrefixes[prefix],
142+
key: `${prefix}${body}`,
143+
}),
144+
);
145+
},
146+
Full: (s): Parser<ApiKeyEntity> => {
147+
return map(s.PrefixKey, (value, b, a) => apiKey(value, b, a));
148+
},
149+
parser: (s): Parser<ApiKeyEntity> => {
150+
return dot(any(s.Full));
151+
},
152+
});

src/CreditCard.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
type Context,
3-
createLanguageThis,
3+
createLanguage,
44
map,
55
regex,
66
} from "@claudiu-ceia/combine";
@@ -55,35 +55,27 @@ const isValidCard = (raw: string): boolean => {
5555
};
5656

5757
type CreditCardLanguage = {
58-
Raw: () => Parser<string>;
59-
Full: () => Parser<CreditCardEntity>;
60-
parser: () => Parser<CreditCardEntity>;
58+
Raw: Parser<string>;
59+
Full: Parser<CreditCardEntity>;
60+
parser: Parser<CreditCardEntity>;
6161
};
6262

6363
/**
6464
* Credit card parser language (13-19 digits with separators) validated via Luhn.
6565
*/
66-
export const CreditCard: ReturnType<
67-
typeof createLanguageThis<CreditCardLanguage>
68-
> = createLanguageThis<CreditCardLanguage>({
69-
Raw(): Parser<string> {
70-
// 13-19 digits with optional single separators (space or '-').
71-
return guard(regex(/\d(?:[ -]?\d){12,18}/, "credit-card"), isValidCard);
72-
},
73-
Full(): Parser<CreditCardEntity> {
74-
return map(
75-
this.Raw,
76-
(raw, b, a) =>
77-
creditCard(
78-
{
79-
digits: normalizeDigits(raw),
80-
},
81-
b,
82-
a,
83-
),
84-
);
85-
},
86-
parser(): Parser<CreditCardEntity> {
87-
return dot(this.Full);
88-
},
66+
export const CreditCard: CreditCardLanguage = createLanguage<
67+
CreditCardLanguage
68+
>({
69+
// 13-19 digits with optional single separators (space or '-').
70+
Raw: () => guard(regex(/\d(?:[ -]?\d){12,18}/, "credit-card"), isValidCard),
71+
Full: (s) =>
72+
map(s.Raw, (raw, b, a) =>
73+
creditCard(
74+
{
75+
digits: normalizeDigits(raw),
76+
},
77+
b,
78+
a,
79+
)),
80+
parser: (s) => dot(s.Full),
8981
});

src/Email.ts

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
any,
33
type Context,
4-
createLanguageThis,
4+
createLanguage,
55
digit,
66
letter,
77
manyTill,
@@ -36,38 +36,34 @@ export const email = (
3636
};
3737

3838
type EmailLanguage = {
39-
Full: () => Parser<EmailEntity>;
40-
parser: () => Parser<EmailEntity>;
39+
Full: Parser<EmailEntity>;
40+
parser: Parser<EmailEntity>;
4141
};
4242

4343
/**
4444
* Email address parser language.
4545
*/
46-
export const Email: ReturnType<typeof createLanguageThis<EmailLanguage>> =
47-
createLanguageThis<EmailLanguage>({
48-
Full() {
49-
return map(
50-
seq(
51-
map(
52-
manyTill(
53-
any(letter(), digit(), str("."), str("-"), str("-"), str("+")),
54-
str("@"),
55-
),
56-
(p) => p.join(""),
46+
export const Email: EmailLanguage = createLanguage<EmailLanguage>({
47+
Full: () =>
48+
map(
49+
seq(
50+
map(
51+
manyTill(
52+
any(letter(), digit(), str("."), str("-"), str("-"), str("+")),
53+
str("@"),
5754
),
58-
URL.Domain,
55+
(p) => p.join(""),
5956
),
60-
([firstPart, domain], b, a) =>
61-
email(
62-
{
63-
email: `${firstPart}${domain}`,
64-
},
65-
b,
66-
a,
67-
),
68-
);
69-
},
70-
parser() {
71-
return dot(any(this.Full));
72-
},
73-
});
57+
URL.Domain,
58+
),
59+
([firstPart, domain], b, a) =>
60+
email(
61+
{
62+
email: `${firstPart}${domain}`,
63+
},
64+
b,
65+
a,
66+
),
67+
),
68+
parser: (s) => dot(any(s.Full)),
69+
});

0 commit comments

Comments
 (0)