GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALRequest.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Text.RegularExpressions;
20
21namespace GenerallyPositive
22{
28 public class GPALCall
29 {
31 public string Url { get; set; }
33 public string Method { get; set; }
35 public string ResourceType { get; set; }
37 public string PostData { get; set; }
39 public string Initiator { get; set; }
44 public int Status { get; set; }
46 internal string RequestId { get; set; }
51 public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
52 }
53
76 public class GPALRequest : IAllowRequestSettings
77 {
79 public string Path { get; internal set; }
80
82 public string Method { get; internal set; } = "GET";
83
85 public string Body { get; internal set; }
86
88 public string ContentType { get; internal set; } = "application/json";
89
91 public List<KeyValuePair<string, string>> Parameters { get; internal set; } = new List<KeyValuePair<string, string>>();
92
94 public List<KeyValuePair<string, string>> Headers { get; internal set; } = new List<KeyValuePair<string, string>>();
95
98 public int FirstPage { get; internal set; }
99
106 public string PageToken { get; internal set; } = "{page}";
107
110
112 public string Name { get; internal set; }
113
114 internal GPALRequest()
115 {
116 }
117
122 public GPALRequest ToGPALObject()
123 {
124 return this;
125 }
126
133 public IAllowRequestSettings WithPath(string path)
134 {
135 Path = path;
136 return this;
137 }
138
144 public IAllowRequestSettings WithHttpMethod(Enums.HttpVerb method)
145 {
146 Method = method;
147 return this;
148 }
149
155 public IAllowRequestSettings WithParameter(string nameValue)
156 {
157 Parameters.Add(SplitPair(nameValue, '='));
158 return this;
159 }
160
199 public IAllowRequestSettings WithTokenFor(string parameterName)
200 {
201 // the next number, counted from what is already tokenized, so calls stack without being told an index
202 int token = 0;
203
204 while (true == Path?.Contains($"{{{token}}}") || true == Body?.Contains($"{{{token}}}"))
205 token++;
206
207 // a value lives in one of two places and a workflow should not have to care which. in a url it is
208 // name=value up to the next &, in a json body it is "name":"value" or "name":value
209 string inUrl = $"([?&]{Regex.Escape(parameterName)}=)[^&]*";
210 string inJson = "(\"" + Regex.Escape(parameterName) + "\"\\s*:\\s*)(\"[^\"]*\"|[^,}\\s]+)";
211
212 if (false == string.IsNullOrEmpty(Path))
213 Path = Regex.Replace(Path, inUrl, "${1}" + $"{{{token}}}");
214
215 if (false == string.IsNullOrEmpty(Body))
216 {
217 Body = Regex.Replace(Body, inUrl, "${1}" + $"{{{token}}}");
218 Body = Regex.Replace(Body, inJson, "${1}" + $"\"{{{token}}}\"");
219 }
220
221 return this;
222 }
223
230 public IAllowRequestSettings WithHeader(string nameValue)
231 {
232 Headers.Add(SplitPair(nameValue, ':'));
233 return this;
234 }
235
242 public IAllowRequestSettings WithBody(string body)
243 {
244 Body = body;
245 return this;
246 }
247
253 public IAllowRequestSettings WithContentType(Enums.ContentType contentType)
254 {
255 ContentType = contentType;
256 return this;
257 }
258
266 {
267 FirstPage = firstPage;
268 return this;
269 }
270
277 public IAllowRequestSettings WithPageToken(string token = "{page}")
278 {
279 PageToken = token;
280 return this;
281 }
282
292 {
293 AfterFetch = callAfterFetch;
294 return this;
295 }
296
302 public IAllowToGPALObject<GPALRequest> WithName(string name)
303 {
304 Name = name;
305 return this;
306 }
307
316 internal string ResolveUrl(int page, List<string> row)
317 {
318 List<string> query = new List<string>();
319
320 foreach (KeyValuePair<string, string> parameter in Parameters)
321 query.Add($"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(Resolve(parameter.Value ?? string.Empty, page, row))}");
322
323 string path = Resolve(Path, page, row);
324
325 return 0 == query.Count
326 ? path
327 : path + (path?.Contains("?") ?? false ? "&" : "?") + string.Join("&", query);
328 }
329
337 internal string ResolveBody(int page, List<string> row)
338 {
339 return Resolve(Body, page, row);
340 }
341
351 string Resolve(string text, int page, List<string> row)
352 {
353 string resolved = text?.Replace(PageToken, (FirstPage + page).ToString());
354
355 if (null != resolved && null != row)
356 for (int token = 0; token < row.Count; token++)
357 resolved = resolved.Replace($"{{{token}}}", row[token] ?? string.Empty);
358
359 return resolved;
360 }
361
367 internal int TokenCount()
368 {
369 int highest = -1;
370
371 foreach (string text in TokenizedText())
372 foreach (Match match in Regex.Matches(text ?? string.Empty, @"\{(\d+)\}"))
373 if (match.Value != PageToken)
374 highest = Math.Max(highest, int.Parse(match.Groups[1].Value));
375
376 return highest + 1;
377 }
378
384 List<string> TokenizedText()
385 {
386 List<string> text = new List<string> { Path, Body };
387
388 foreach (KeyValuePair<string, string> parameter in Parameters)
389 text.Add(parameter.Value);
390
391 foreach (KeyValuePair<string, string> header in Headers)
392 text.Add(header.Value);
393
394 return text;
395 }
396
404 internal string[] HeaderPairs(int page, List<string> row)
405 {
406 List<string> pairs = new List<string>();
407
408 foreach (KeyValuePair<string, string> header in Headers)
409 {
410 pairs.Add(header.Key);
411 pairs.Add(Resolve(header.Value, page, row));
412 }
413
414 return pairs.ToArray();
415 }
416
424 static KeyValuePair<string, string> SplitPair(string pair, char separator)
425 {
426 KeyValuePair<string, string> split = new KeyValuePair<string, string>(pair, string.Empty);
427 int at = pair?.IndexOf(separator) ?? -1;
428
429 if (0 <= at)
430 split = new KeyValuePair<string, string>(pair.Substring(0, at).Trim(), pair.Substring(at + 1).Trim());
431
432 return split;
433 }
434 }
435}
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Definition Browser.cs:68
delegate CallIfStatus CallAfterFetchDelegate(IBrowser browser, List< string > results, IGPALGrid< string > tokens, int tokenIdx)
Delegate callback for the CallAfterFetch handler declared on a GPALRequest, invoked after each row of...
One request the page made, as reported by the browser while .CaptureCalls was on. Where a GPALRequest...
string ResourceType
What the browser was fetching it as: XHR, Fetch, Document, Script, Image and the rest.
int Status
The HTTP status the site answered with, or 0 when nothing came back yet. What the page itself got,...
string Initiator
What set it off: parser, script, preload or other.
string Url
The full url requested.
string PostData
The request body, when it had one. Null for a GET.
Dictionary< string, string > Headers
The headers the page sent with it. What its own code added is here, which is what an API expects and ...
string Method
The HTTP method, GET, POST and so on.
Describes an API request for .Fetch to issue from inside the page, so it carries the session the brow...
IAllowRequestSettings WithContentType(Enums.ContentType contentType)
Content type of the body. application/json when nothing was said.
string ContentType
Content type of the body.
IAllowRequestSettings WithParameter(string nameValue)
Adds a query parameter, written as "name=value". Call once per parameter.
Browser.Browser.CallAfterFetchDelegate AfterFetch
Called once per row of tokens, with that row's pages. Null when nothing was said.
IAllowToGPALObject< GPALRequest > WithName(string name)
Names the request for messages and errors. Narrows the interface, so it goes last.
string PageToken
Marks where the page number goes. Put it anywhere .Fetch sends: the path, a parameter value,...
IAllowRequestSettings WithHeader(string nameValue)
Adds a request header, written as "Name: value". Call once per header. The browser sends its own head...
IAllowRequestSettings WithHttpMethod(Enums.HttpVerb method)
HTTP method to use, for example GET, POST, PUT or DELETE. GET when nothing was said.
IAllowRequestSettings WithPath(string path)
Path or full URL to request. A path such as "/api/search" is resolved against the origin the browser ...
IAllowRequestSettings WithBody(string body)
Body to send, used as-is. Sets the content type to application/json unless .WithContentType says othe...
IAllowRequestSettings WithTokenFor(string parameterName)
Replaces the value of a query parameter with the next numbered token, so a request taken from a page ...
string Method
HTTP method. GET when nothing was said.
List< KeyValuePair< string, string > > Headers
Extra request headers, on top of whatever the browser sends for itself.
List< KeyValuePair< string, string > > Parameters
Query parameters, in the order they were added.
string Path
Path or full URL to request. A path rides the origin the browser is already on.
string Name
Name used in messages and errors.
IAllowRequestSettings WithPageToken(string token="{page}")
The marker replaced with the page number, wherever it appears in the path, a parameter value,...
IAllowRequestSettings WithFirstPage(int firstPage)
What this API calls its first page, which is what the page token counts on from. Zero for Algolia and...
IAllowRequestSettings CallAfterFetch(Browser.Browser.CallAfterFetchDelegate callAfterFetch)
Called after each row of tokens has been fetched, with that row's pages, so the workflow can work the...
int FirstPage
What the API calls its first page. Zero for Algolia and most search backends, one for many others....
string Body
Request body, sent as-is.
GPALRequest ToGPALObject()
Returns this request, so a chain can be assigned without an explicit cast.