OCaml client for the LinkedIn Voyager API
1type node = Soup.general Soup.node
2
3type 'a t = node -> ('a, string) result
4(** A decoder takes a DOM node (subtree root) and produces either a value or an
5 error message describing what it couldn't find. *)
6
7let return v _node = Ok v
8let map f dec node = match dec node with Ok v -> Ok (f v) | Error _ as e -> e
9
10let both a b node =
11 match a node with
12 | Error _ as e -> e
13 | Ok av -> ( match b node with Error _ as e -> e | Ok bv -> Ok (av, bv))
14
15let apply fd ad node =
16 match fd node with
17 | Error _ as e -> e
18 | Ok f -> ( match ad node with Error _ as e -> e | Ok v -> Ok (f v))
19
20let ( let+ ) x f = map f x
21let ( and+ ) = both
22let default v dec node = match dec node with Ok _ as r -> r | Error _ -> Ok v
23
24let fallback decs node =
25 let rec loop = function
26 | [] -> Error "fallback: all decoders failed"
27 | dec :: rest -> (
28 match dec node with Ok _ as r -> r | Error _ -> loop rest)
29 in
30 loop decs
31
32(** {1 Leaf decoders} *)
33
34let text node =
35 match Soup.trimmed_texts node with
36 | [] -> Ok ""
37 | parts -> Ok (String.concat " " parts)
38
39let attr_opt name node =
40 match Soup.element node with
41 | None -> Ok None
42 | Some el -> Ok (Soup.attribute name el)
43
44let attr name node =
45 match attr_opt name node with
46 | Ok (Some v) -> Ok v
47 | Ok None -> Error (Fmt.str "missing attribute %S" name)
48 | Error _ as e -> e
49
50let html node = Ok (Soup.to_string node)
51
52(** {1 Navigation} *)
53
54let coerce_soup (el : Soup.element Soup.node) : node = (Soup.coerce el : node)
55
56let query_opt selector dec node =
57 match Soup.( $? ) node selector with
58 | None -> Ok None
59 | Some el -> (
60 match dec (coerce_soup el) with
61 | Ok v -> Ok (Some v)
62 | Error e -> Error (Fmt.str "in %S: %s" selector e))
63
64let query selector dec node =
65 match Soup.( $? ) node selector with
66 | None -> Error (Fmt.str "no match for selector %S" selector)
67 | Some el -> (
68 match dec (coerce_soup el) with
69 | Ok _ as r -> r
70 | Error e -> Error (Fmt.str "in %S: %s" selector e))
71
72let query_all selector dec node =
73 let nodes = Soup.( $$ ) node selector |> Soup.to_list in
74 let rec loop acc = function
75 | [] -> Ok (List.rev acc)
76 | el :: rest -> (
77 match dec (coerce_soup el) with
78 | Error e -> Error (Fmt.str "in %S: %s" selector e)
79 | Ok v -> loop (v :: acc) rest)
80 in
81 loop [] nodes
82
83(** {1 Running} *)
84
85let run_on_soup dec (soup : Soup.soup Soup.node) = dec (Soup.coerce soup)
86
87let of_string dec html =
88 let soup = Soup.parse html in
89 run_on_soup dec soup