Add await registry-informed getTypes method with smart entity tag validation

fuck type coloring madness
This commit is contained in:
2024-05-12 02:10:15 -05:00
parent 15666716a6
commit b3048086c3
4 changed files with 84 additions and 33 deletions

View File

@@ -46,7 +46,7 @@ type LookupInputProps = {
onChange?: (target: {
target: string;
targetType: TargetType | null;
}) => void;
}) => Promise<void>;
detectedType: Maybe<TargetType>;
};
@@ -155,7 +155,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
required: true,
onChange: () => {
if (onChange != undefined)
onChange({
void onChange({
target: getValues("target"),
// dropdown target will be pulled from state anyways, so no need to provide it here
targetType: retrieveTargetType(null),
@@ -172,7 +172,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
setSelected(value);
if (onChange != undefined)
onChange({
void onChange({
target: getValues("target"),
// we provide the value as the state will not have updated yet for this context
targetType: retrieveTargetType(value),

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { domainMatchPredicate, getBestURL, getType } from "@/rdap";
import type {
AutonomousNumber,
@@ -43,19 +43,19 @@ const useLookup = (warningHandler?: WarningHandler) => {
const [error, setError] = useState<string | null>(null);
const [target, setTarget] = useState<string>("");
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
// Used by a callback on LookupInput to forcibly set the type of the lookup.
const [currentType, setTargetType] = useState<TargetType | null>(null);
// Used to allow repeatable lookups when weird errors happen.
const repeatableRef = useRef<string>("");
const uriType = useMemo<Maybe<TargetType>>(
function () {
if (currentType != null) return Maybe.just(currentType);
return getType(target).mapOr(Maybe.nothing(), (type) => Maybe.just(type));
},
[target, currentType]
);
useCallback(async () => {
if (currentType != null) return Maybe.just(currentType);
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(Maybe.nothing(), (type) => Maybe.just(type));
setUriType(uri);
}, [target, currentType])
// Fetch & load a specific registry's data into memory.
async function loadBootstrap(type: RootRegistryType, force = false) {
@@ -81,6 +81,20 @@ const useLookup = (warningHandler?: WarningHandler) => {
};
}
async function getRegistry(type: RootRegistryType): Promise<Register> {
if (registryDataRef.current[type] == null) await loadBootstrap(type);
if (registryDataRef.current[type] == null)
throw new Error(
`Could not load bootstrap data for ${type} registry.`
);
return registryDataRef.current[type];
}
async function getTypeEasy(target: string): Promise<Result<TargetType, Error>> {
return getType(target, getRegistry);
}
function getRegistryURL(
type: RootRegistryType,
lookupTarget: string
@@ -232,7 +246,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
new Error("A target must be given in order to execute a lookup.")
);
const targetType = getType(target);
const targetType = await getTypeEasy(target);
if (targetType.isErr) {
return Result.err(
@@ -365,7 +379,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
}
}
return { error, setTarget, setTargetType, submit, currentType: uriType };
return { error, setTarget, setTargetType, submit, currentType: uriType, getType: getTypeEasy };
};
export default useLookup;

View File

@@ -9,10 +9,9 @@ import LookupInput from "@/components/form/LookupInput";
import ErrorCard from "@/components/common/ErrorCard";
import { Maybe } from "true-myth";
import type { TargetType } from "@/types";
import { getType } from "@/rdap";
const Index: NextPage = () => {
const { error, setTarget, setTargetType, submit } = useLookup();
const { error, setTarget, setTargetType, submit, getType } = useLookup();
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(
Maybe.nothing()
);
@@ -46,11 +45,11 @@ const Index: NextPage = () => {
<LookupInput
isLoading={isLoading}
detectedType={detectedType}
onChange={({ target, targetType }) => {
onChange={async ({ target, targetType }) => {
setTarget(target);
setTargetType(targetType);
const detectResult = getType(target);
const detectResult = await getType(target);
if (detectResult.isOk) {
setDetectedType(Maybe.just(detectResult.value));
} else {

View File

@@ -1,4 +1,4 @@
import type { TargetType } from "@/types";
import type { Register, RootRegistryType, TargetType } from "@/types";
import { Result } from "true-myth";
// const cardTitles = {
@@ -757,19 +757,54 @@ export function createRDAPLink(url, title) {
}
*/
const TypeValidators: Record<TargetType, (value: string) => boolean> = {
autnum: (value) => /^AS\d+$/.test(value),
ip4: (value) => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/?\d*$/.test(value),
ip6: (value) => /^[0-9a-f:]{2,}\/?\d*$/.test(value),
url: (value) => /^https?:/.test(value),
json: (value) => /^{/.test(value),
tld: (value) => /^\.\w+$/.test(value),
domain: (value) =>
/[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?/.test(
value
),
entity: (value) => false,
registrar: (value) => false,
type ValidatorArgs = {
value: string;
getRegistry: (type: RootRegistryType) => Promise<Register>;
};
const TypeValidators: Record<
TargetType,
(args: ValidatorArgs) => Promise<boolean>
> = {
autnum: ({ value }) => Promise.resolve(/^AS\d+$/.test(value)),
ip4: ({ value }) =>
Promise.resolve(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/?\d*$/.test(value)),
ip6: ({ value }) => Promise.resolve(/^[0-9a-f:]{2,}\/?\d*$/.test(value)),
url: ({ value }) => Promise.resolve(/^https?:/.test(value)),
json: ({ value }) => Promise.resolve(/^{/.test(value)),
tld: ({ value }) => Promise.resolve(/^\.\w+$/.test(value)),
domain: ({ value }) => {
return Promise.resolve(
/[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?/.test(
value
)
);
},
entity: async ({ value, getRegistry }) => {
// Ensure the entity handle is in the correct format
const result = value.match(/^\w+-(\w+)$/);
if (result === null || result.length <= 1 || result[1] == undefined) return false;
// Check if the entity object tag is real
try {
const registry = await getRegistry("entity");
// Check each service to see if tag starts with inputted value
for (const service of registry.services) {
const tags = service[1];
console.log({tags, result});
if (tags.some((tag) => tag.startsWith(result[1] as string))) return true;
}
return false;
} catch (e) {
console.error(new Error("Failed to fetch entity registry", {cause: e}));
return false;
}
return true;
},
registrar: ({ }) => Promise.resolve(false),
};
/**
@@ -779,9 +814,12 @@ const TypeValidators: Record<TargetType, (value: string) => boolean> = {
* @returns A `Result` object containing the determined `TargetType` if a match is found,
* otherwise an `Error` object.
*/
export function getType(value: string): Result<TargetType, Error> {
export async function getType(
value: string,
getRegistry: (type: RootRegistryType) => Promise<Register>
): Promise<Result<TargetType, Error>> {
for (const [type, validator] of Object.entries(TypeValidators)) {
if (validator(value)) return Result.ok(type);
if (await validator({ value, getRegistry })) return Result.ok(type);
}
return Result.err(new Error("No patterns matched the input"));
}