···11+namespace PDSharp.Core
22+33+open System
44+open System.Text.Json
55+66+module Lexicon =
77+ type LexiconResult =
88+ | Ok
99+ | Error of string
1010+1111+ module Validation =
1212+ let private getProperty (p : string) (element : JsonElement) =
1313+ match element.TryGetProperty(p) with
1414+ | true, prop -> Some prop
1515+ | _ -> None
1616+1717+ let private getString (p : string) (element : JsonElement) =
1818+ match getProperty p element with
1919+ | Some prop when prop.ValueKind = JsonValueKind.String -> Some(prop.GetString())
2020+ | _ -> None
2121+2222+ let private validateStringField
2323+ (element : JsonElement)
2424+ (fieldName : string)
2525+ (maxLength : int option)
2626+ (required : bool)
2727+ : LexiconResult =
2828+ match getProperty fieldName element with
2929+ | Some prop ->
3030+ if prop.ValueKind <> JsonValueKind.String then
3131+ Error $"Field '{fieldName}' must be a string"
3232+ else
3333+ match maxLength with
3434+ | Some maxLen when prop.GetString().Length > maxLen ->
3535+ Error $"Field '{fieldName}' exceeds maximum length of {maxLen}"
3636+ | _ -> Ok
3737+ | None ->
3838+ if required then
3939+ Error $"Missing required field '{fieldName}'"
4040+ else
4141+ Ok
4242+4343+ let private validateIsoDate (element : JsonElement) (fieldName : string) (required : bool) : LexiconResult =
4444+ match getProperty fieldName element with
4545+ | Some prop ->
4646+ if prop.ValueKind <> JsonValueKind.String then
4747+ Error $"Field '{fieldName}' must be a string"
4848+ else
4949+ let s = prop.GetString()
5050+ let mutable dt = DateTimeOffset.MinValue
5151+5252+ if DateTimeOffset.TryParse(s, &dt) then
5353+ Ok
5454+ else
5555+ Error $"Field '{fieldName}' must be a valid ISO 8601 date string"
5656+ | None ->
5757+ if required then
5858+ Error $"Missing required field '{fieldName}'"
5959+ else
6060+ Ok
6161+6262+ let private validateRef (element : JsonElement) (fieldName : string) (required : bool) : LexiconResult =
6363+ match getProperty fieldName element with
6464+ | Some prop ->
6565+ if prop.ValueKind <> JsonValueKind.Object then
6666+ Error $"Field '{fieldName}' must be an object"
6767+ else
6868+ match validateStringField prop "uri" None true, validateStringField prop "cid" None true with
6969+ | Ok, Ok -> Ok
7070+ | Error e, _ -> Error $"Field '{fieldName}': {e}"
7171+ | _, Error e -> Error $"Field '{fieldName}': {e}"
7272+ | None ->
7373+ if required then
7474+ Error $"Missing required field '{fieldName}'"
7575+ else
7676+ Ok
7777+7878+ let validatePost (record : JsonElement) : LexiconResult =
7979+ let textCheck = validateStringField record "text" (Some 3000) true
8080+ let dateCheck = validateIsoDate record "createdAt" true
8181+8282+ match textCheck, dateCheck with
8383+ | Ok, Ok -> Ok
8484+ | Error e, _ -> Error e
8585+ | _, Error e -> Error e
8686+8787+ let validateLike (record : JsonElement) : LexiconResult =
8888+ let subjectCheck = validateRef record "subject" true
8989+ let dateCheck = validateIsoDate record "createdAt" true
9090+9191+ match subjectCheck, dateCheck with
9292+ | Ok, Ok -> Ok
9393+ | Error e, _ -> Error e
9494+ | _, Error e -> Error e
9595+9696+ let validateRepost (record : JsonElement) : LexiconResult =
9797+ let subjectCheck = validateRef record "subject" true
9898+ let dateCheck = validateIsoDate record "createdAt" true
9999+100100+ match subjectCheck, dateCheck with
101101+ | Ok, Ok -> Ok
102102+ | Error e, _ -> Error e
103103+ | _, Error e -> Error e
104104+105105+ let validateFollow (record : JsonElement) : LexiconResult =
106106+ let subjectCheck = validateStringField record "subject" None true
107107+ let dateCheck = validateIsoDate record "createdAt" true
108108+109109+ match subjectCheck, dateCheck with
110110+ | Ok, Ok -> Ok
111111+ | Error e, _ -> Error e
112112+ | _, Error e -> Error e
113113+114114+ let validateProfile (record : JsonElement) : LexiconResult =
115115+ let nameCheck = validateStringField record "displayName" (Some 640) false
116116+ let descCheck = validateStringField record "description" (Some 2560) false
117117+118118+ match nameCheck, descCheck with
119119+ | Ok, Ok -> Ok
120120+ | Error e, _ -> Error e
121121+ | _, Error e -> Error e
122122+123123+ /// Unknown records are valid but unvalidated.
124124+ let validate (collection : string) (record : JsonElement) : LexiconResult =
125125+ match collection with
126126+ | "app.bsky.feed.post" -> Validation.validatePost record
127127+ | "app.bsky.feed.like" -> Validation.validateLike record
128128+ | "app.bsky.feed.repost" -> Validation.validateRepost record
129129+ | "app.bsky.graph.follow" -> Validation.validateFollow record
130130+ | "app.bsky.actor.profile" -> Validation.validateProfile record
131131+ | _ -> Ok