[{"data":1,"prerenderedAt":1541},["ShallowReactive",2],{"\u002Fwritings\u002Foctodin":3},{"id":4,"title":5,"body":6,"date":1530,"description":1531,"extension":1532,"meta":1533,"navigation":1534,"path":1535,"seo":1536,"stem":1537,"tag":1538,"ttr":1539,"__hash__":1540},"writing\u002Fwritings\u002Foctodin.md","How to talk to GitHub with no HTTP client",{"type":7,"value":8,"toc":1520},"minimark",[9,14,26,29,55,58,61,65,88,94,97,126,129,133,152,308,315,333,337,340,343,442,446,468,486,503,507,525,568,572,575,971,977,1147,1157,1381,1384,1489,1492,1496,1499,1513,1516],[10,11,13],"h2",{"id":12},"a-small-recap","A small recap",[15,16,17,18,25],"p",{},"I've been spending time with ",[19,20,24],"a",{"href":21,"rel":22},"https:\u002F\u002Fodin-lang.org\u002F",[23],"nofollow","Odin"," lately, a language that feels like it was designed by someone who got tired of C's nonsense but refused to give up C's honesty. No hidden allocations, no garbage collector pausing your program to take out the trash, no twenty layers of abstraction between you and the machine.",[15,27,28],{},"I like that. I'm a difficult man about these things.",[15,30,31,32,40,41,45,46,49,50,54],{},"Alongside it, I've been building ",[33,34,35],"strong",{},[19,36,39],{"href":37,"rel":38},"https:\u002F\u002Fgithub.com\u002Frivethorn\u002Fmimir",[23],"Mimir",", a little toolchain to make Odin projects pleasant to manage; ",[42,43,44],"code",{},"mimir run",", ",[42,47,48],{},"mimir build",", the basics, done right. And at some point, as these things go, I needed a side project to ",[51,52,53],"em",{},"use"," the toolchain on. Something real enough to expose rough edges, small enough to finish.",[15,56,57],{},"A GitHub API client seemed perfect. Every ecosystem has its Octokit. Odin didn't have one.",[15,59,60],{},"That should have been a weekend project. It was not a weekend project.",[10,62,64],{"id":63},"the-gap","The gap",[15,66,67,68,71,72,75,76,79,80,83,84,87],{},"Here's the thing nobody tells you when you arrive in Odin-land: ",[33,69,70],{},"there is no HTTP client in core",". No ",[42,73,74],{},"net\u002Fhttp",". No TLS stack. The ",[42,77,78],{},"core:net"," package gives you sockets and DNS, and ",[42,81,82],{},"core:crypto"," gives you primitives; but if you want to ",[42,85,86],{},"GET https:\u002F\u002Fapi.github.com",", you are on your own.",[15,89,90,91],{},"My first thought was something along the lines of: ",[51,92,93],{},"\"Alright… where's the challenge?\"",[15,95,96],{},"The realistic options were:",[98,99,100,107,117],"ol",{},[101,102,103,106],"li",{},[33,104,105],{},"Write HTTPS from scratch",": sockets + TLS by hand. Educational, character-building, and completely insane for a side project.",[101,108,109,112,113,116],{},[33,110,111],{},"Shell out to"," ",[42,114,115],{},"curl",": pragmatic, ugly, and a process spawn per request. No.",[101,118,119,112,122,125],{},[33,120,121],{},"Use",[42,123,124],{},"vendor:curl",": Odin ships bindings to libcurl, which is already on every macOS and Linux machine. HTTPS, redirects, timeouts, all solved by battle-tested C code.",[15,127,128],{},"I went with option three. It felt like the high road: own the design, rent the transport.",[10,130,132],{"id":131},"the-transport","The transport",[15,134,135,136,139,140,143,144,147,148,151],{},"If you've never driven libcurl's \"easy\" interface from a language with explicit allocators, it's a small adventure. You hand C a function pointer and a ",[42,137,138],{},"void*",", and C calls you back with chunks of bytes. The catch: Odin's dynamic arrays allocate through ",[42,141,142],{},"context.allocator",", and inside a ",[42,145,146],{},"\"c\""," callback there ",[51,149,150],{},"is"," no context. The fix is a small ritual: stash a copy of the caller's context in the buffer struct, restore it inside the callback:",[153,154,159],"pre",{"className":155,"code":156,"language":157,"meta":158,"style":158},"language-odin shiki shiki-themes gruvbox-light-hard gruvbox-dark-hard gruvbox-dark-hard","write_cb :: proc \"c\" (ptr: rawptr, size, nmemb: c.size_t, userdata: rawptr) -> c.size_t {\n    buf := (^Curl_Buffer)(userdata)\n    context = buf.ctx\n    ...\n}\n","odin","",[42,160,161,249,275,293,302],{"__ignoreMap":158},[162,163,166,170,174,178,182,186,189,192,196,199,202,205,208,210,213,215,218,221,224,226,229,231,233,236,239,241,243,246],"span",{"class":164,"line":165},"line",1,[162,167,169],{"class":168},"sXcZX","write_cb",[162,171,173],{"class":172},"sHSTz"," ::",[162,175,177],{"class":176},"sTCDi"," proc",[162,179,181],{"class":180},"swc6o"," \"",[162,183,185],{"class":184},"snxCs","c",[162,187,188],{"class":180},"\"",[162,190,191],{"class":180}," (",[162,193,195],{"class":194},"sgL72","ptr",[162,197,198],{"class":172},":",[162,200,201],{"class":168}," rawptr",[162,203,204],{"class":180},",",[162,206,207],{"class":194}," size",[162,209,204],{"class":180},[162,211,212],{"class":194}," nmemb",[162,214,198],{"class":172},[162,216,217],{"class":194}," c",[162,219,220],{"class":180},".",[162,222,223],{"class":168},"size_t",[162,225,204],{"class":180},[162,227,228],{"class":194}," userdata",[162,230,198],{"class":172},[162,232,201],{"class":168},[162,234,235],{"class":180},")",[162,237,238],{"class":176}," ->",[162,240,217],{"class":194},[162,242,220],{"class":180},[162,244,245],{"class":168},"size_t ",[162,247,248],{"class":180},"{\n",[162,250,252,255,258,260,263,266,269,272],{"class":164,"line":251},2,[162,253,254],{"class":194},"    buf",[162,256,257],{"class":172}," :=",[162,259,191],{"class":180},[162,261,262],{"class":172},"^",[162,264,265],{"class":194},"Curl_Buffer",[162,267,268],{"class":180},")(",[162,270,271],{"class":194},"userdata",[162,273,274],{"class":180},")\n",[162,276,278,282,285,288,290],{"class":164,"line":277},3,[162,279,281],{"class":280},"sA8KM","    context",[162,283,284],{"class":172}," =",[162,286,287],{"class":194}," buf",[162,289,220],{"class":180},[162,291,292],{"class":194},"ctx\n",[162,294,296,299],{"class":164,"line":295},4,[162,297,298],{"class":172},"    ..",[162,300,301],{"class":180},".\n",[162,303,305],{"class":164,"line":304},5,[162,306,307],{"class":180},"}\n",[15,309,310,311,314],{},"It's five lines of code and it took me an embarrassingly long time to be ",[51,312,313],{},"sure"," about. That's Odin in a nutshell: nothing is magic, so everything is understandable; but you do have to actually understand it.",[15,316,317,318,321,322,325,326,45,329,332],{},"Once bytes flow, the rest is plumbing with opinions: ",[42,319,320],{},"Accept: application\u002Fvnd.github+json"," and the API-version header on every request, ",[42,323,324],{},"Authorization: Bearer"," when a token is present, redirect following, per-request timeouts, and rate-limit headers (",[42,327,328],{},"X-RateLimit-Remaining",[42,330,331],{},"Retry-After",", the lot) parsed off every response. GitHub practically begs you to respect its quotas, so the client makes it hard not to. Every response carries its quota state with it.",[10,334,336],{"id":335},"the-design","The design",[15,338,339],{},"I wanted the library to feel like Odin, not like a Go library wearing an Odin costume. They do tend to look alike.",[15,341,342],{},"That meant a few deliberate choices:",[344,345,346,385,395,426],"ul",{},[101,347,348,351,354,356,357,360,361,364,365,368,369,372,373,376,377,380,381,384],{},[33,349,350],{},"Every endpoint returns",[42,352,353],{},"(value, Response, Error)",[33,355,220],{}," The ",[42,358,359],{},"Response"," stays yours even when decoding fails, so you can always inspect the status, headers, and rate limits. The ",[42,362,363],{},"Error"," is a union: ",[42,366,367],{},"Transport_Error"," for the network, ",[42,370,371],{},"Api_Error"," for the API's own complaints, ",[42,374,375],{},"Codec_Error"," for JSON, with helpers like ",[42,378,379],{},"is_not_found"," and ",[42,382,383],{},"is_rate_limited"," for the branches you actually write.",[101,386,387,390,391,394],{},[33,388,389],{},"Timestamps are strings."," GitHub returns RFC 3339; parsing them is ",[51,392,393],{},"your"," decision, not the library's tax on every call. This is the kind of restraint I wish more libraries had.",[101,396,397,112,400,112,403,112,406,409,410,413,414,417,418,421,422,425],{},[42,398,399],{},"Maybe(T)",[33,401,402],{},"+",[42,404,405],{},"omitempty",[33,407,408],{},"turned out to be the perfect PATCH semantics."," This was my favorite discovery. Odin's JSON marshaller omits nil",[42,411,412],{},"Maybe ","values, so an update struct with all-",[42,415,416],{},"Maybe","fields serializes ",[51,419,420],{},"exactly"," the fields you set, including an explicit",[42,423,424],{},"false",". No builder pattern, no pointer soup, no \"was this field set?\" bookkeeping. The language just… had the feature. I tested the round-trip to be sure, and it was one of those moments where the tool earns your trust.",[101,427,428,431,432,45,435,45,438,441],{},[33,429,430],{},"Memory is explicit."," Every decoded type has a matching destructor: ",[42,433,434],{},"destroy_repository",[42,436,437],{},"destroy_issues",[42,439,440],{},"destroy_search_users",". Nothing is freed behind your back, and if you back the client with an arena allocator, you can skip the destructors entirely. It reads as boilerplate until the first time you run with a tracking allocator and watch everything balance to zero. Then it reads as craftsmanship.",[10,443,445],{"id":444},"the-library-ification","The library-ification",[15,447,448,449,452,453,456,457,460,461,464,465,220],{},"The project started life as a binary with the library hiding in ",[42,450,451],{},"pkgs\u002F",". But Mimir recently grew a ",[42,454,455],{},"-lib"," flag (",[42,458,459],{},"mimir new -lib","), so I converted OctOdin into a proper library: sources at the root, ",[42,462,463],{},"package octodin",", and the demo as a nested example project under ",[42,466,467],{},"example\u002F",[15,469,470,471,380,474,477,478,481,482,485],{},"That conversion caught real toolchain gaps, by the way. Mimir's project check wants ",[42,472,473],{},"src\u002F",[42,475,476],{},"ols.json"," in the working directory, so a pure library root isn't directly buildable yet, and neither is ",[42,479,480],{},"mimir run example"," from the root. The example runs from its own directory, and ",[42,483,484],{},"odin build . -build-mode:obj"," type-checks the lib. Eating your own dogfood works: Mimir has a small backlog now.",[15,487,488,489,492,493,498,499,502],{},"The example also loads its token from an ",[42,490,491],{},".env"," file via my ",[19,494,497],{"href":495,"rel":496},"https:\u002F\u002Fgithub.com\u002Frivethorn\u002Fdotenv-odin",[23],"dotenv-odin"," library; one call at startup, ",[42,500,501],{},"GITHUB_TOKEN"," appears in the environment, missing file silently ignored. Small tools composing with small tools. That's the whole philosophy.",[10,504,506],{"id":505},"and-so-octodin-was-born","And so… OctOdin was born",[15,508,509,514,515,45,518,45,521,524],{},[19,510,513],{"href":511,"rel":512},"https:\u002F\u002Fgithub.com\u002Frivethorn\u002Foctodin",[23],"OctOdin"," covers repositories, issues, pull requests, users, releases (including asset uploads), Actions, search, and the system endpoints; with a generic layer (",[42,516,517],{},"do_request",[42,519,520],{},"get_into",[42,522,523],{},"send_json",") for everything it doesn't cover yet. The demo hits entirely public endpoints, so it runs with no token at all:",[153,526,530],{"className":527,"code":528,"language":529,"meta":158,"style":158},"language-shell shiki shiki-themes gruvbox-light-hard gruvbox-dark-hard gruvbox-dark-hard","cd example\nmimir run               # repository overview\nmimir run -- extended   # issues, search, contents, errors\n","shell",[42,531,532,540,552],{"__ignoreMap":158},[162,533,534,537],{"class":164,"line":165},[162,535,536],{"class":176},"cd",[162,538,539],{"class":184}," example\n",[162,541,542,545,548],{"class":164,"line":251},[162,543,544],{"class":168},"mimir",[162,546,547],{"class":184}," run",[162,549,551],{"class":550},"sIIdi","               # repository overview\n",[162,553,554,556,558,562,565],{"class":164,"line":277},[162,555,544],{"class":168},[162,557,547],{"class":184},[162,559,561],{"class":560},"sYNTs"," --",[162,563,564],{"class":184}," extended",[162,566,567],{"class":550},"   # issues, search, contents, errors\n",[10,569,571],{"id":570},"what-using-it-looks-like","What using it looks like",[15,573,574],{},"Enough about the plumbing. The point of all that restraint is that call sites stay small. A basic fetch is one call, one error branch, and a row of defers:",[153,576,578],{"className":155,"code":577,"language":157,"meta":158,"style":158},"import \"pkgs:octodin\"\n\nclient := octodin.client_make(octodin.Config{token = os.get_env(\"GITHUB_TOKEN\", context.temp_allocator)})\ndefer octodin.client_destroy(&client)\n\nrepo, resp, err := octodin.get_repo(&client, \"odin-lang\", \"Odin\")\nif err != nil {\n    msg := octodin.error_message(err)\n    defer delete(msg)\n    fmt.eprintfln(\"failed: %s\", msg)\n    return\n}\ndefer octodin.response_destroy(&resp)\ndefer octodin.destroy_repository(&repo)\n\nfmt.println(repo.full_name, repo.stargazers_count)\nfmt.println(\"rate:\", resp.rate.remaining, \"\u002F\", resp.rate.limit)\n",[42,579,580,593,599,667,688,692,742,759,781,797,827,833,838,859,878,883,915],{"__ignoreMap":158},[162,581,582,585,587,590],{"class":164,"line":165},[162,583,584],{"class":280},"import",[162,586,181],{"class":180},[162,588,589],{"class":184},"pkgs:octodin",[162,591,592],{"class":180},"\"\n",[162,594,595],{"class":164,"line":251},[162,596,598],{"emptyLinePlaceholder":597},true,"\n",[162,600,601,604,606,609,611,614,618,621,623,626,629,632,634,637,639,642,644,646,648,650,652,655,657,660,662,665],{"class":164,"line":277},[162,602,603],{"class":194},"client",[162,605,257],{"class":172},[162,607,608],{"class":194}," octodin",[162,610,220],{"class":180},[162,612,613],{"class":168},"client_make",[162,615,617],{"class":616},"s2xuh","(",[162,619,620],{"class":194},"octodin",[162,622,220],{"class":180},[162,624,625],{"class":194},"Config",[162,627,628],{"class":180},"{",[162,630,631],{"class":194},"token",[162,633,284],{"class":172},[162,635,636],{"class":194}," os",[162,638,220],{"class":180},[162,640,641],{"class":168},"get_env",[162,643,617],{"class":616},[162,645,188],{"class":180},[162,647,501],{"class":184},[162,649,188],{"class":180},[162,651,204],{"class":180},[162,653,654],{"class":280}," context",[162,656,220],{"class":180},[162,658,659],{"class":194},"temp_allocator",[162,661,235],{"class":616},[162,663,664],{"class":180},"}",[162,666,274],{"class":616},[162,668,669,672,674,676,679,681,684,686],{"class":164,"line":295},[162,670,671],{"class":280},"defer",[162,673,608],{"class":194},[162,675,220],{"class":180},[162,677,678],{"class":168},"client_destroy",[162,680,617],{"class":616},[162,682,683],{"class":172},"&",[162,685,603],{"class":194},[162,687,274],{"class":616},[162,689,690],{"class":164,"line":304},[162,691,598],{"emptyLinePlaceholder":597},[162,693,695,698,700,703,705,708,710,712,714,717,719,721,723,725,727,730,732,734,736,738,740],{"class":164,"line":694},6,[162,696,697],{"class":194},"repo",[162,699,204],{"class":180},[162,701,702],{"class":194}," resp",[162,704,204],{"class":180},[162,706,707],{"class":194}," err",[162,709,257],{"class":172},[162,711,608],{"class":194},[162,713,220],{"class":180},[162,715,716],{"class":168},"get_repo",[162,718,617],{"class":616},[162,720,683],{"class":172},[162,722,603],{"class":194},[162,724,204],{"class":180},[162,726,181],{"class":180},[162,728,729],{"class":184},"odin-lang",[162,731,188],{"class":180},[162,733,204],{"class":180},[162,735,181],{"class":180},[162,737,24],{"class":184},[162,739,188],{"class":180},[162,741,274],{"class":616},[162,743,745,748,750,753,756],{"class":164,"line":744},7,[162,746,747],{"class":280},"if",[162,749,707],{"class":194},[162,751,752],{"class":172}," !=",[162,754,755],{"class":560}," nil",[162,757,758],{"class":180}," {\n",[162,760,762,765,767,769,771,774,776,779],{"class":164,"line":761},8,[162,763,764],{"class":194},"    msg",[162,766,257],{"class":172},[162,768,608],{"class":194},[162,770,220],{"class":180},[162,772,773],{"class":168},"error_message",[162,775,617],{"class":616},[162,777,778],{"class":194},"err",[162,780,274],{"class":616},[162,782,784,787,790,792,795],{"class":164,"line":783},9,[162,785,786],{"class":280},"    defer",[162,788,789],{"class":176}," delete",[162,791,617],{"class":616},[162,793,794],{"class":194},"msg",[162,796,274],{"class":616},[162,798,800,803,805,808,810,812,815,818,820,822,825],{"class":164,"line":799},10,[162,801,802],{"class":194},"    fmt",[162,804,220],{"class":180},[162,806,807],{"class":168},"eprintfln",[162,809,617],{"class":616},[162,811,188],{"class":180},[162,813,814],{"class":184},"failed: ",[162,816,817],{"class":560},"%s",[162,819,188],{"class":180},[162,821,204],{"class":180},[162,823,824],{"class":194}," msg",[162,826,274],{"class":616},[162,828,830],{"class":164,"line":829},11,[162,831,832],{"class":280},"    return\n",[162,834,836],{"class":164,"line":835},12,[162,837,307],{"class":180},[162,839,841,843,845,847,850,852,854,857],{"class":164,"line":840},13,[162,842,671],{"class":280},[162,844,608],{"class":194},[162,846,220],{"class":180},[162,848,849],{"class":168},"response_destroy",[162,851,617],{"class":616},[162,853,683],{"class":172},[162,855,856],{"class":194},"resp",[162,858,274],{"class":616},[162,860,862,864,866,868,870,872,874,876],{"class":164,"line":861},14,[162,863,671],{"class":280},[162,865,608],{"class":194},[162,867,220],{"class":180},[162,869,434],{"class":168},[162,871,617],{"class":616},[162,873,683],{"class":172},[162,875,697],{"class":194},[162,877,274],{"class":616},[162,879,881],{"class":164,"line":880},15,[162,882,598],{"emptyLinePlaceholder":597},[162,884,886,889,891,894,896,898,900,903,905,908,910,913],{"class":164,"line":885},16,[162,887,888],{"class":194},"fmt",[162,890,220],{"class":180},[162,892,893],{"class":168},"println",[162,895,617],{"class":616},[162,897,697],{"class":194},[162,899,220],{"class":180},[162,901,902],{"class":194},"full_name",[162,904,204],{"class":180},[162,906,907],{"class":194}," repo",[162,909,220],{"class":180},[162,911,912],{"class":194},"stargazers_count",[162,914,274],{"class":616},[162,916,918,920,922,924,926,928,931,933,935,937,939,942,944,947,949,951,954,956,958,960,962,964,966,969],{"class":164,"line":917},17,[162,919,888],{"class":194},[162,921,220],{"class":180},[162,923,893],{"class":168},[162,925,617],{"class":616},[162,927,188],{"class":180},[162,929,930],{"class":184},"rate:",[162,932,188],{"class":180},[162,934,204],{"class":180},[162,936,702],{"class":194},[162,938,220],{"class":180},[162,940,941],{"class":194},"rate",[162,943,220],{"class":180},[162,945,946],{"class":194},"remaining",[162,948,204],{"class":180},[162,950,181],{"class":180},[162,952,953],{"class":184},"\u002F",[162,955,188],{"class":180},[162,957,204],{"class":180},[162,959,702],{"class":194},[162,961,220],{"class":180},[162,963,941],{"class":194},[162,965,220],{"class":180},[162,967,968],{"class":194},"limit",[162,970,274],{"class":616},[15,972,973,974,976],{},"That triple ",[42,975,353],{}," is the whole library in miniature. The value is yours to destroy, the response is yours to inspect, and the error is a union you can actually branch on instead of string-matching:",[153,978,980],{"className":155,"code":979,"language":157,"meta":158,"style":158},"repo, resp, err := octodin.get_repo(&client, owner, name)\nif err != nil {\n    if octodin.is_not_found(err) {\n        fmt.println(\"no such repo — check the name\")\n    } else if octodin.is_rate_limited(err) {\n        fmt.println(\"slow down; the quota says so\")\n    }\n    octodin.destroy_error(&err)\n    return\n}\n",[42,981,982,1020,1032,1052,1072,1097,1116,1121,1139,1143],{"__ignoreMap":158},[162,983,984,986,988,990,992,994,996,998,1000,1002,1004,1006,1008,1010,1013,1015,1018],{"class":164,"line":165},[162,985,697],{"class":194},[162,987,204],{"class":180},[162,989,702],{"class":194},[162,991,204],{"class":180},[162,993,707],{"class":194},[162,995,257],{"class":172},[162,997,608],{"class":194},[162,999,220],{"class":180},[162,1001,716],{"class":168},[162,1003,617],{"class":616},[162,1005,683],{"class":172},[162,1007,603],{"class":194},[162,1009,204],{"class":180},[162,1011,1012],{"class":194}," owner",[162,1014,204],{"class":180},[162,1016,1017],{"class":194}," name",[162,1019,274],{"class":616},[162,1021,1022,1024,1026,1028,1030],{"class":164,"line":251},[162,1023,747],{"class":280},[162,1025,707],{"class":194},[162,1027,752],{"class":172},[162,1029,755],{"class":560},[162,1031,758],{"class":180},[162,1033,1034,1037,1039,1041,1043,1045,1047,1050],{"class":164,"line":277},[162,1035,1036],{"class":280},"    if",[162,1038,608],{"class":194},[162,1040,220],{"class":180},[162,1042,379],{"class":168},[162,1044,617],{"class":616},[162,1046,778],{"class":194},[162,1048,1049],{"class":616},") ",[162,1051,248],{"class":180},[162,1053,1054,1057,1059,1061,1063,1065,1068,1070],{"class":164,"line":295},[162,1055,1056],{"class":194},"        fmt",[162,1058,220],{"class":180},[162,1060,893],{"class":168},[162,1062,617],{"class":616},[162,1064,188],{"class":180},[162,1066,1067],{"class":184},"no such repo — check the name",[162,1069,188],{"class":180},[162,1071,274],{"class":616},[162,1073,1074,1077,1080,1083,1085,1087,1089,1091,1093,1095],{"class":164,"line":304},[162,1075,1076],{"class":180},"    }",[162,1078,1079],{"class":280}," else",[162,1081,1082],{"class":280}," if",[162,1084,608],{"class":194},[162,1086,220],{"class":180},[162,1088,383],{"class":168},[162,1090,617],{"class":616},[162,1092,778],{"class":194},[162,1094,1049],{"class":616},[162,1096,248],{"class":180},[162,1098,1099,1101,1103,1105,1107,1109,1112,1114],{"class":164,"line":694},[162,1100,1056],{"class":194},[162,1102,220],{"class":180},[162,1104,893],{"class":168},[162,1106,617],{"class":616},[162,1108,188],{"class":180},[162,1110,1111],{"class":184},"slow down; the quota says so",[162,1113,188],{"class":180},[162,1115,274],{"class":616},[162,1117,1118],{"class":164,"line":744},[162,1119,1120],{"class":180},"    }\n",[162,1122,1123,1126,1128,1131,1133,1135,1137],{"class":164,"line":761},[162,1124,1125],{"class":194},"    octodin",[162,1127,220],{"class":180},[162,1129,1130],{"class":168},"destroy_error",[162,1132,617],{"class":616},[162,1134,683],{"class":172},[162,1136,778],{"class":194},[162,1138,274],{"class":616},[162,1140,1141],{"class":164,"line":783},[162,1142,832],{"class":280},[162,1144,1145],{"class":164,"line":799},[162,1146,307],{"class":180},[15,1148,1149,1150,1153,1154,198],{},"And pagination respects the protocol instead of hiding it. List endpoints take option structs, and the ",[42,1151,1152],{},"Link"," header comes back parsed, so walking pages is just a loop over ",[42,1155,1156],{},"next_page_path",[153,1158,1160],{"className":155,"code":1159,"language":157,"meta":158,"style":158},"issues, resp, err := octodin.list_repo_issues(\n    &client, \"odin-lang\", \"Odin\",\n    {per_page = 50, state = \"open\"},\n)\n\u002F\u002F ... use issues, then destroy both ...\n\n\u002F\u002F More pages? The next path drops straight back into the generic layer:\nfor octodin.page_info(resp).has_next {\n    next := octodin.next_page_path(&client, resp)\n    defer delete(next)\n    page, page_resp, page_err := octodin.get_into(&client, next, []octodin.Issue)\n    \u002F\u002F ... use page, then destroy all three ...\n}\n",[42,1161,1162,1187,1213,1243,1247,1252,1256,1261,1286,1311,1324,1372,1377],{"__ignoreMap":158},[162,1163,1164,1167,1169,1171,1173,1175,1177,1179,1181,1184],{"class":164,"line":165},[162,1165,1166],{"class":194},"issues",[162,1168,204],{"class":180},[162,1170,702],{"class":194},[162,1172,204],{"class":180},[162,1174,707],{"class":194},[162,1176,257],{"class":172},[162,1178,608],{"class":194},[162,1180,220],{"class":180},[162,1182,1183],{"class":168},"list_repo_issues",[162,1185,1186],{"class":616},"(\n",[162,1188,1189,1192,1194,1196,1198,1200,1202,1204,1206,1208,1210],{"class":164,"line":251},[162,1190,1191],{"class":172},"    &",[162,1193,603],{"class":194},[162,1195,204],{"class":180},[162,1197,181],{"class":180},[162,1199,729],{"class":184},[162,1201,188],{"class":180},[162,1203,204],{"class":180},[162,1205,181],{"class":180},[162,1207,24],{"class":184},[162,1209,188],{"class":180},[162,1211,1212],{"class":180},",\n",[162,1214,1215,1218,1221,1223,1226,1228,1231,1233,1235,1238,1240],{"class":164,"line":277},[162,1216,1217],{"class":180},"    {",[162,1219,1220],{"class":194},"per_page",[162,1222,284],{"class":172},[162,1224,1225],{"class":560}," 50",[162,1227,204],{"class":180},[162,1229,1230],{"class":194}," state",[162,1232,284],{"class":172},[162,1234,181],{"class":180},[162,1236,1237],{"class":184},"open",[162,1239,188],{"class":180},[162,1241,1242],{"class":180},"},\n",[162,1244,1245],{"class":164,"line":295},[162,1246,274],{"class":616},[162,1248,1249],{"class":164,"line":304},[162,1250,1251],{"class":550},"\u002F\u002F ... use issues, then destroy both ...\n",[162,1253,1254],{"class":164,"line":694},[162,1255,598],{"emptyLinePlaceholder":597},[162,1257,1258],{"class":164,"line":744},[162,1259,1260],{"class":550},"\u002F\u002F More pages? The next path drops straight back into the generic layer:\n",[162,1262,1263,1266,1268,1270,1273,1275,1277,1279,1281,1284],{"class":164,"line":761},[162,1264,1265],{"class":280},"for",[162,1267,608],{"class":194},[162,1269,220],{"class":180},[162,1271,1272],{"class":168},"page_info",[162,1274,617],{"class":616},[162,1276,856],{"class":194},[162,1278,235],{"class":616},[162,1280,220],{"class":180},[162,1282,1283],{"class":194},"has_next",[162,1285,758],{"class":180},[162,1287,1288,1291,1293,1295,1297,1299,1301,1303,1305,1307,1309],{"class":164,"line":783},[162,1289,1290],{"class":194},"    next",[162,1292,257],{"class":172},[162,1294,608],{"class":194},[162,1296,220],{"class":180},[162,1298,1156],{"class":168},[162,1300,617],{"class":616},[162,1302,683],{"class":172},[162,1304,603],{"class":194},[162,1306,204],{"class":180},[162,1308,702],{"class":194},[162,1310,274],{"class":616},[162,1312,1313,1315,1317,1319,1322],{"class":164,"line":799},[162,1314,786],{"class":280},[162,1316,789],{"class":176},[162,1318,617],{"class":616},[162,1320,1321],{"class":194},"next",[162,1323,274],{"class":616},[162,1325,1326,1329,1331,1334,1336,1339,1341,1343,1345,1347,1349,1351,1353,1355,1358,1360,1363,1365,1367,1370],{"class":164,"line":829},[162,1327,1328],{"class":194},"    page",[162,1330,204],{"class":180},[162,1332,1333],{"class":194}," page_resp",[162,1335,204],{"class":180},[162,1337,1338],{"class":194}," page_err",[162,1340,257],{"class":172},[162,1342,608],{"class":194},[162,1344,220],{"class":180},[162,1346,520],{"class":168},[162,1348,617],{"class":616},[162,1350,683],{"class":172},[162,1352,603],{"class":194},[162,1354,204],{"class":180},[162,1356,1357],{"class":194}," next",[162,1359,204],{"class":180},[162,1361,1362],{"class":616}," []",[162,1364,620],{"class":194},[162,1366,220],{"class":180},[162,1368,1369],{"class":194},"Issue",[162,1371,274],{"class":616},[162,1373,1374],{"class":164,"line":835},[162,1375,1376],{"class":550},"    \u002F\u002F ... use page, then destroy all three ...\n",[162,1378,1379],{"class":164,"line":840},[162,1380,307],{"class":180},[15,1382,1383],{},"My favorite one-liner is search, because the query builder keeps GitHub's qualifier syntax readable instead of percent-encoded:",[153,1385,1387],{"className":155,"code":1386,"language":157,"meta":158,"style":158},"query := octodin.search_terms({\"language:odin\", \"stars:>100\"}, context.temp_allocator)\nresults, resp, err := octodin.search_repositories(&client, {q = query, per_page = 5})\n",[42,1388,1389,1434],{"__ignoreMap":158},[162,1390,1391,1394,1396,1398,1400,1403,1405,1407,1409,1412,1414,1416,1418,1421,1423,1426,1428,1430,1432],{"class":164,"line":165},[162,1392,1393],{"class":194},"query",[162,1395,257],{"class":172},[162,1397,608],{"class":194},[162,1399,220],{"class":180},[162,1401,1402],{"class":168},"search_terms",[162,1404,617],{"class":616},[162,1406,628],{"class":180},[162,1408,188],{"class":180},[162,1410,1411],{"class":184},"language:odin",[162,1413,188],{"class":180},[162,1415,204],{"class":180},[162,1417,181],{"class":180},[162,1419,1420],{"class":184},"stars:>100",[162,1422,188],{"class":180},[162,1424,1425],{"class":180},"},",[162,1427,654],{"class":280},[162,1429,220],{"class":180},[162,1431,659],{"class":194},[162,1433,274],{"class":616},[162,1435,1436,1439,1441,1443,1445,1447,1449,1451,1453,1456,1458,1460,1462,1464,1467,1470,1472,1475,1477,1480,1482,1485,1487],{"class":164,"line":251},[162,1437,1438],{"class":194},"results",[162,1440,204],{"class":180},[162,1442,702],{"class":194},[162,1444,204],{"class":180},[162,1446,707],{"class":194},[162,1448,257],{"class":172},[162,1450,608],{"class":194},[162,1452,220],{"class":180},[162,1454,1455],{"class":168},"search_repositories",[162,1457,617],{"class":616},[162,1459,683],{"class":172},[162,1461,603],{"class":194},[162,1463,204],{"class":180},[162,1465,1466],{"class":180}," {",[162,1468,1469],{"class":194},"q",[162,1471,284],{"class":172},[162,1473,1474],{"class":194}," query",[162,1476,204],{"class":180},[162,1478,1479],{"class":194}," per_page",[162,1481,284],{"class":172},[162,1483,1484],{"class":560}," 5",[162,1486,664],{"class":180},[162,1488,274],{"class":616},[15,1490,1491],{},"Nothing clever. That's the compliment.",[10,1493,1495],{"id":1494},"epilogue","Epilogue",[15,1497,1498],{},"Like most tools, this one exists because the ecosystem had a hole shaped exactly like my curiosity.",[15,1500,1501,1502,1507,1508,1512],{},"If you're an Odin developer or just curious what deliberate, magic-free API design feels like, I'd love to hear your ",[19,1503,1506],{"href":1504,"rel":1505},"https:\u002F\u002Fgithub.com\u002Frivethorn\u002Foctodin\u002Fissues\u002Fnew",[23],"suggestions"," or ",[19,1509,1511],{"href":1510},"mailto:hassanak813@outlook.com","general thoughts"," about OctOdin.",[15,1514,1515],{},"Build slowly, and own your stack.",[1517,1518,1519],"style",{},"html pre.shiki code .sTCDi, html code.shiki .sTCDi{--shiki-light:#AF3A03;--shiki-default:#FE8019;--shiki-dark:#FE8019}html pre.shiki code .snxCs, html code.shiki .snxCs{--shiki-light:#79740E;--shiki-default:#B8BB26;--shiki-dark:#B8BB26}html pre.shiki code .sXcZX, html code.shiki .sXcZX{--shiki-light:#B57614;--shiki-default:#FABD2F;--shiki-dark:#FABD2F}html pre.shiki code .sIIdi, html code.shiki .sIIdi{--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-default:#928374;--shiki-default-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic}html pre.shiki code .sYNTs, html code.shiki .sYNTs{--shiki-light:#8F3F71;--shiki-default:#D3869B;--shiki-dark:#D3869B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHSTz, html code.shiki .sHSTz{--shiki-light:#427B58;--shiki-default:#8EC07C;--shiki-dark:#8EC07C}html pre.shiki code .swc6o, html code.shiki .swc6o{--shiki-light:#7C6F64;--shiki-default:#A89984;--shiki-dark:#A89984}html pre.shiki code .sgL72, html code.shiki .sgL72{--shiki-light:#076678;--shiki-default:#83A598;--shiki-dark:#83A598}html pre.shiki code .sA8KM, html code.shiki .sA8KM{--shiki-light:#9D0006;--shiki-default:#FB4934;--shiki-dark:#FB4934}html pre.shiki code .s2xuh, html code.shiki .s2xuh{--shiki-light:#3C3836;--shiki-default:#EBDBB2;--shiki-dark:#EBDBB2}",{"title":158,"searchDepth":251,"depth":277,"links":1521},[1522,1523,1524,1525,1526,1527,1528,1529],{"id":12,"depth":251,"text":13},{"id":63,"depth":251,"text":64},{"id":131,"depth":251,"text":132},{"id":335,"depth":251,"text":336},{"id":444,"depth":251,"text":445},{"id":505,"depth":251,"text":506},{"id":570,"depth":251,"text":571},{"id":1494,"depth":251,"text":1495},"2026-10-13","How a missing piece in Odin's standard library turned into OctOdin — a GitHub REST API client I actually enjoy using.","md",{},{"title":5,"description":1531},"\u002Fwritings\u002Foctodin",{"title":5,"description":1531},"writings\u002Foctodin","Development","5 min read","sGmgiTzzWwRPBYdTcMRCb-bhUoD7w6xAiaScU8pTSNQ",1791855502806]