Add feed validation and display
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import {now, uniq, displayList} from "@welshman/lib"
|
||||
import {
|
||||
FeedType,
|
||||
Feed,
|
||||
AddressFeed,
|
||||
AuthorFeed,
|
||||
CreatedAtFeed,
|
||||
DVMFeed,
|
||||
DifferenceFeed,
|
||||
IDFeed,
|
||||
IntersectionFeed,
|
||||
GlobalFeed,
|
||||
KindFeed,
|
||||
ListFeed,
|
||||
LabelFeed,
|
||||
WOTFeed,
|
||||
RelayFeed,
|
||||
ScopeFeed,
|
||||
SearchFeed,
|
||||
TagFeed,
|
||||
UnionFeed,
|
||||
} from "./core.js"
|
||||
import {getFeedArgs} from "./utils.js"
|
||||
|
||||
export const displayAddressFeed = (feed: AddressFeed) => {
|
||||
const n = getFeedArgs(feed).length
|
||||
|
||||
return `${n} replaceable {n === 1 ? 'event' : 'events'}`
|
||||
}
|
||||
|
||||
export const displayAuthorFeed = (feed: AuthorFeed) => {
|
||||
const n = getFeedArgs(feed).length
|
||||
|
||||
return `events from ${n} replaceable {n === 1 ? 'person' : 'people'}`
|
||||
}
|
||||
|
||||
export const displayCreatedAtFeed = (feed: CreatedAtFeed) => {
|
||||
const items = getFeedArgs(feed)
|
||||
const descriptions: string[] = []
|
||||
|
||||
for (const {since, until, relative = []} of items) {
|
||||
const parts: string[] = []
|
||||
|
||||
if (since) {
|
||||
const timestamp = relative.includes("since") ? now() - since : since
|
||||
parts.push(`after ${new Date(timestamp * 1000).toLocaleString()}`)
|
||||
}
|
||||
|
||||
if (until) {
|
||||
const timestamp = relative.includes("until") ? now() - until : until
|
||||
parts.push(`before ${new Date(timestamp * 1000).toLocaleString()}`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
descriptions.push(parts.join(" and "))
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptions.length === 0) {
|
||||
return "events from any time"
|
||||
}
|
||||
|
||||
return `events ${displayList(descriptions, "or")}`
|
||||
}
|
||||
|
||||
export const displayDVMFeed = (feed: DVMFeed) => {
|
||||
const items = getFeedArgs(feed)
|
||||
const descriptions: string[] = []
|
||||
|
||||
for (const {kind, tags = [], relays = []} of items) {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push(`kind ${kind}`)
|
||||
|
||||
if (tags.length > 0) {
|
||||
parts.push(`with ${tags.length} tag${tags.length === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
if (relays.length > 0) {
|
||||
parts.push(`from ${displayList(relays)}`)
|
||||
}
|
||||
|
||||
descriptions.push(parts.join(" "))
|
||||
}
|
||||
|
||||
return `events from DVM requests of ${displayList(descriptions)}`
|
||||
}
|
||||
|
||||
export const displayDifferenceFeed = (feed: DifferenceFeed) => {
|
||||
const [base, ...excluded] = getFeedArgs(feed)
|
||||
|
||||
return `all ${displayFeed(base)}, excluding ${displayList(excluded.map(displayFeed))}`
|
||||
}
|
||||
|
||||
export const displayIDFeed = (feed: IDFeed) => `${getFeedArgs(feed).length} events`
|
||||
|
||||
export const displayIntersectionFeed = (feed: IntersectionFeed) =>
|
||||
`events matching ${displayList(getFeedArgs(feed).map(displayFeed))}`
|
||||
|
||||
export const displayGlobalFeed = (feed: GlobalFeed) => "anything"
|
||||
|
||||
export const displayKindFeed = (feed: KindFeed) =>
|
||||
`events of kind ${displayList(getFeedArgs(feed))}`
|
||||
|
||||
export const displayListFeed = (feed: ListFeed) => {
|
||||
const addresses = uniq(getFeedArgs(feed).flatMap(({addresses}) => addresses))
|
||||
|
||||
return `events from ${addresses.length} list${addresses.length === 1 ? "" : "s"}`
|
||||
}
|
||||
|
||||
export const displayLabelFeed = (feed: LabelFeed) => {
|
||||
const items = getFeedArgs(feed)
|
||||
const descriptions: string[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const parts: string[] = []
|
||||
|
||||
if (item.authors?.length) {
|
||||
parts.push(`by ${item.authors.length} author${item.authors.length === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
const tags = Object.entries(item)
|
||||
.filter(([key]) => key.startsWith("#"))
|
||||
.map(([key, values]) => `${key}=${displayList(values as string[])}`)
|
||||
|
||||
if (tags.length) {
|
||||
parts.push(`with tags ${displayList(tags)}`)
|
||||
}
|
||||
|
||||
if (item.relays?.length) {
|
||||
parts.push(`from ${displayList(item.relays)}`)
|
||||
}
|
||||
|
||||
descriptions.push(parts.join(" "))
|
||||
}
|
||||
|
||||
return `events ${displayList(descriptions)}`
|
||||
}
|
||||
|
||||
export const displayWOTFeed = (feed: WOTFeed) => {
|
||||
const descriptions = getFeedArgs(feed).map(({min = 0, max = 1}) =>
|
||||
min === max ? `WOT score of ${min}` : `WOT score between ${min} and ${max}`,
|
||||
)
|
||||
|
||||
return `Events from authors with ${displayList(descriptions)}`
|
||||
}
|
||||
|
||||
export const displayRelayFeed = (feed: RelayFeed) => `events from ${displayList(getFeedArgs(feed))}`
|
||||
|
||||
export const displayScopeFeed = (feed: ScopeFeed) =>
|
||||
`events from ${displayList(getFeedArgs(feed).map(s => s.toLowerCase()))}`
|
||||
|
||||
export const displaySearchFeed = (feed: SearchFeed) =>
|
||||
`events matching ${displayList(getFeedArgs(feed).map(term => `"${term}"`))}`
|
||||
|
||||
export const displayTagFeed = (feed: TagFeed) => {
|
||||
const [key, ...values] = getFeedArgs(feed)
|
||||
|
||||
return `events with ${key} tag matching ${displayList(values, "or")}`
|
||||
}
|
||||
|
||||
export const displayUnionFeed = (feed: UnionFeed) =>
|
||||
`all ${displayList(getFeedArgs(feed).map(displayFeed))}`
|
||||
|
||||
export const displayFeed = (feed: Feed): string => {
|
||||
switch (feed[0]) {
|
||||
case FeedType.Address:
|
||||
return displayAddressFeed(feed)
|
||||
case FeedType.Author:
|
||||
return displayAuthorFeed(feed)
|
||||
case FeedType.CreatedAt:
|
||||
return displayCreatedAtFeed(feed)
|
||||
case FeedType.DVM:
|
||||
return displayDVMFeed(feed)
|
||||
case FeedType.Difference:
|
||||
return displayDifferenceFeed(feed)
|
||||
case FeedType.ID:
|
||||
return displayIDFeed(feed)
|
||||
case FeedType.Intersection:
|
||||
return displayIntersectionFeed(feed)
|
||||
case FeedType.Global:
|
||||
return displayGlobalFeed(feed)
|
||||
case FeedType.Kind:
|
||||
return displayKindFeed(feed)
|
||||
case FeedType.List:
|
||||
return displayListFeed(feed)
|
||||
case FeedType.Label:
|
||||
return displayLabelFeed(feed)
|
||||
case FeedType.WOT:
|
||||
return displayWOTFeed(feed)
|
||||
case FeedType.Relay:
|
||||
return displayRelayFeed(feed)
|
||||
case FeedType.Scope:
|
||||
return displayScopeFeed(feed)
|
||||
case FeedType.Search:
|
||||
return displaySearchFeed(feed)
|
||||
case FeedType.Tag:
|
||||
return displayTagFeed(feed)
|
||||
case FeedType.Union:
|
||||
return displayUnionFeed(feed)
|
||||
default:
|
||||
return "[unknown feed type]"
|
||||
}
|
||||
}
|
||||
|
||||
export const displayFeeds = (feeds: Feed[]) => displayList(feeds.map(displayFeed))
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from "./core.js"
|
||||
export * from "./compiler.js"
|
||||
export * from "./controller.js"
|
||||
export * from "./display.js"
|
||||
export * from "./utils.js"
|
||||
export * from "./validate.js"
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import Trava from "trava"
|
||||
import {spec, isPojo} from "@welshman/lib"
|
||||
import {isRelayUrl, Address} from "@welshman/util"
|
||||
import {Scope, FeedType} from "./core.js"
|
||||
import {getFeedArgs} from "./utils.js"
|
||||
|
||||
const {ValidationError, Compose, Check, Each, Optional, Keys} = Trava
|
||||
|
||||
const validateNumber = Check((x: any) => typeof x === "number", "Value must be a number")
|
||||
|
||||
const validateString = Check((x: any) => typeof x === "string", "Value must be a string")
|
||||
|
||||
const validateRelay = Check((x: any) => typeof x === "string" && isRelayUrl(x), "Invalid relay url")
|
||||
|
||||
const validateScope = Check((x: any) => Object.values(Scope).includes(x), "Invalid scope")
|
||||
|
||||
const validateHex = Check((x: any) => typeof x === "string" && x.length === 64, "Invalid hex value")
|
||||
|
||||
const validateAddress = Check(
|
||||
(x: any) => typeof x === "string" && Address.isAddress(x),
|
||||
"Invalid address",
|
||||
)
|
||||
|
||||
const validateArray = Check((x: any) => Array.isArray(x), "Value must be an array")
|
||||
|
||||
const validateObject = Check((x: any) => isPojo(x), "Value must be an object")
|
||||
|
||||
export {ValidationError}
|
||||
|
||||
export const validateTagFeedMapping = Compose([
|
||||
validateArray,
|
||||
Check(spec({length: 2}), "Tag feed mappings must have two entries"),
|
||||
(a: any[]) => validateString(a[0]),
|
||||
(a: any[]) => validateFeed(a[1]),
|
||||
])
|
||||
|
||||
export const validateFeedArgs = (validateArgument: Trava.Validator) => (feed: any) => {
|
||||
let error = validateArray(feed)
|
||||
if (error instanceof ValidationError) return error
|
||||
|
||||
for (const argument of getFeedArgs(feed)) {
|
||||
error = validateArgument(argument)
|
||||
if (error instanceof ValidationError) return error
|
||||
}
|
||||
|
||||
return feed
|
||||
}
|
||||
|
||||
export const validateAddressFeed = validateFeedArgs(validateAddress)
|
||||
|
||||
export const validateAuthorFeed = validateFeedArgs(validateHex)
|
||||
|
||||
export const validateCreatedAtFeed = validateFeedArgs(
|
||||
Keys({
|
||||
since: Optional(validateNumber),
|
||||
until: Optional(validateNumber),
|
||||
relative: Optional(Each(validateString)),
|
||||
}),
|
||||
)
|
||||
|
||||
export const validateDVMFeed = validateFeedArgs(
|
||||
Keys({
|
||||
kind: validateNumber,
|
||||
tags: Optional(Each(validateString)),
|
||||
relays: Optional(Each(validateString)),
|
||||
mappings: Optional(Each(validateTagFeedMapping)),
|
||||
}),
|
||||
)
|
||||
|
||||
export const validateDifferenceFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||
|
||||
export const validateIDFeed = validateFeedArgs(validateHex)
|
||||
|
||||
export const validateIntersectionFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||
|
||||
export const validateGlobalFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||
|
||||
export const validateKindFeed = validateFeedArgs(validateNumber)
|
||||
|
||||
export const validateListFeed = validateFeedArgs(
|
||||
Keys({
|
||||
addresses: Optional(Each(validateString)),
|
||||
mappings: Optional(Each(validateTagFeedMapping)),
|
||||
}),
|
||||
)
|
||||
|
||||
export const validateLabelFeed = validateFeedArgs(
|
||||
Compose([
|
||||
validateObject,
|
||||
(item: any) => {
|
||||
const validateRelays = Each(validateRelay)
|
||||
const validateAuthors = Each(validateHex)
|
||||
const validateStrings = Each(validateString)
|
||||
const validateMappings = Each(validateTagFeedMapping)
|
||||
|
||||
for (const [key, value] of Object.entries(item)) {
|
||||
let error
|
||||
if (key === "relays") {
|
||||
error = validateRelays(value)
|
||||
} else if (key === "authors") {
|
||||
error = validateAuthors(value)
|
||||
} else if (key === "mappings") {
|
||||
error = validateMappings(value)
|
||||
} else if (key.match("^#.$")) {
|
||||
error = validateStrings(value)
|
||||
} else {
|
||||
error = new ValidationError("Invalid label item")
|
||||
}
|
||||
|
||||
if (error instanceof ValidationError) return error
|
||||
}
|
||||
|
||||
return item
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
export const validateWOTFeed = validateFeedArgs(
|
||||
Keys({
|
||||
min: Optional(validateNumber),
|
||||
max: Optional(validateNumber),
|
||||
}),
|
||||
)
|
||||
|
||||
export const validateRelayFeed = validateFeedArgs(validateRelay)
|
||||
|
||||
export const validateScopeFeed = validateFeedArgs(validateScope)
|
||||
|
||||
export const validateSearchFeed = validateFeedArgs(validateString)
|
||||
|
||||
export const validateTagFeed = validateFeedArgs(validateString)
|
||||
|
||||
export const validateUnionFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||
|
||||
export const validateFeed = (feed: any) => {
|
||||
const error = validateArray(feed)
|
||||
if (error instanceof ValidationError) return error
|
||||
|
||||
switch (feed[0]) {
|
||||
case FeedType.Address:
|
||||
return validateAddressFeed(feed)
|
||||
case FeedType.Author:
|
||||
return validateAuthorFeed(feed)
|
||||
case FeedType.CreatedAt:
|
||||
return validateCreatedAtFeed(feed)
|
||||
case FeedType.DVM:
|
||||
return validateDVMFeed(feed)
|
||||
case FeedType.Difference:
|
||||
return validateDifferenceFeed(feed)
|
||||
case FeedType.ID:
|
||||
return validateIDFeed(feed)
|
||||
case FeedType.Intersection:
|
||||
return validateIntersectionFeed(feed)
|
||||
case FeedType.Global:
|
||||
return validateGlobalFeed(feed)
|
||||
case FeedType.Kind:
|
||||
return validateKindFeed(feed)
|
||||
case FeedType.List:
|
||||
return validateListFeed(feed)
|
||||
case FeedType.Label:
|
||||
return validateLabelFeed(feed)
|
||||
case FeedType.WOT:
|
||||
return validateWOTFeed(feed)
|
||||
case FeedType.Relay:
|
||||
return validateRelayFeed(feed)
|
||||
case FeedType.Scope:
|
||||
return validateScopeFeed(feed)
|
||||
case FeedType.Search:
|
||||
return validateSearchFeed(feed)
|
||||
case FeedType.Tag:
|
||||
return validateTagFeed(feed)
|
||||
case FeedType.Union:
|
||||
return validateUnionFeed(feed)
|
||||
default:
|
||||
return new ValidationError("Unknown feed type")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user