irongit

A Neovim plugin enabling you to solve LeetCode problems.

251 lines6.4 KBLua
1local curl = require("plenary.curl")
2local log = require("leetcode.logger")
3local config = require("leetcode.config")
4local headers = require("leetcode.api.headers")
5local urls = require("leetcode.api.urls")
6
7---@class lc.Api.Utils
8local utils = {}
9
10---@param endpoint string
11function utils.put(endpoint, opts)
12 local options = vim.tbl_deep_extend("force", {
13 endpoint = endpoint,
14 }, opts or {})
15
16 return utils.curl("put", options)
17end
18
19---@param endpoint string
20function utils.post(endpoint, opts)
21 local options = vim.tbl_deep_extend("force", {
22 endpoint = endpoint,
23 }, opts or {})
24
25 return utils.curl("post", options)
26end
27
28---@param endpoint string
29---@param opts? table
30function utils.get(endpoint, opts)
31 local options = vim.tbl_deep_extend("force", {
32 endpoint = endpoint,
33 }, opts or {})
34
35 return utils.curl("get", options)
36end
37
38---@param query string
39---@param variables? table optional
40---@param opts? { callback?: function, endpoint?: string }
41function utils.query(query, variables, opts)
42 opts = vim.tbl_deep_extend("force", {
43 body = {
44 query = query,
45 variables = variables,
46 },
47 }, opts or {})
48
49 return utils.curl("post", opts)
50end
51
52---@private
53---@param method string
54---@param params table
55function utils.curl(method, params)
56 local params_cpy = vim.deepcopy(params)
57
58 params = vim.tbl_deep_extend("force", {
59 headers = headers.get(),
60 compressed = false,
61 retry = 5,
62 endpoint = urls.base,
63 }, params or {})
64 local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint)
65
66 if type(params.body) == "table" then
67 params.body = vim.json.encode(params.body)
68 end
69
70 local tries = params.retry
71 local function should_retry(err)
72 return err and err.status >= 500 and tries > 0
73 end
74
75 if params.callback then
76 local cb = vim.schedule_wrap(params.callback)
77 params.callback = function(out, _)
78 local res, err = utils.handle_res(out)
79
80 if should_retry(err) then
81 log.debug("retry " .. tries)
82 params_cpy.retry = tries - 1
83 utils.curl(method, params_cpy)
84 else
85 cb(res, err)
86 end
87 end
88
89 curl[method](url, params)
90 else
91 local out = curl[method](url, params)
92 local res, err = utils.handle_res(out)
93
94 if should_retry(err) then
95 log.debug("retry " .. tries)
96 params_cpy.retry = tries - 1
97 utils.curl(method, params_cpy)
98 else
99 return res, err
100 end
101 end
102end
103
104---@private
105---@return table, lc.err
106function utils.handle_res(out)
107 local res, err
108 log.debug(out)
109
110 if out.exit ~= 0 then
111 err = {
112 code = out.exit,
113 msg = "curl failed",
114 }
115 elseif out.status >= 300 then
116 local ok, msg = pcall(function()
117 local dec = utils.decode(out.body)
118
119 if dec.error then
120 return dec.error
121 end
122
123 local tbl = {}
124 for _, e in ipairs(dec.errors) do
125 table.insert(tbl, e.message)
126 end
127
128 return table.concat(tbl, "\n")
129 end)
130
131 res = out.body
132 err = {
133 code = 0,
134 status = out.status,
135 response = out,
136 msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""),
137 out = out.body,
138 }
139 else
140 res = utils.decode(out.body)
141 end
142
143 return res, utils.check_err(err)
144end
145
146---@param err lc.err
147function utils.check_err(err)
148 if not err then
149 return
150 end
151
152 if err.status then
153 if err.status == 401 or err.status == 403 then
154 -- require("leetcode.command").expire()
155 err.msg =
156 "Your cookie may have expired, or LeetCode has temporarily restricted API access"
157 end
158 end
159
160 return err
161end
162
163function utils.decode(str)
164 return vim.json.decode(str)
165 -- local ok, res = pcall(vim.json.decode, str)
166 -- assert(ok, str)
167 -- return res
168end
169
170function utils.normalize_similar_cn(s)
171 s = select(2, pcall(utils.decode, s))
172
173 return vim.tbl_map(function(sq)
174 return {
175 title = sq.title,
176 translated_title = sq.translatedTitle,
177 paid_only = sq.isPaidOnly,
178 title_slug = sq.titleSlug,
179 difficulty = sq.difficulty,
180 }
181 end, s)
182end
183
184function utils.lvl_to_name(lvl)
185 return ({ "Easy", "Medium", "Hard" })[lvl]
186end
187
188---@return lc.cache.Question[]
189function utils.normalize_problems(problems)
190 problems = vim.tbl_filter(function(p)
191 return not p.stat.question__hide
192 end, problems)
193
194 local comp = function(a, b)
195 local a_fid = a.stat.frontend_question_id
196 local b_fid = b.stat.frontend_question_id
197
198 local is_num_a = tonumber(a_fid)
199 local is_num_b = tonumber(b_fid)
200
201 if is_num_a and is_num_b then
202 return tonumber(a_fid) < tonumber(b_fid)
203 elseif is_num_a then
204 return true
205 elseif is_num_b then
206 return false
207 else
208 return a_fid < b_fid
209 end
210 end
211 table.sort(problems, comp)
212
213 return vim.tbl_map(function(p)
214 return {
215 status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
216 id = p.stat.question_id,
217 frontend_id = p.stat.frontend_question_id,
218 title = p.stat.question__title,
219 title_cn = "",
220 title_slug = p.stat.question__title_slug,
221 link = ("https://leetcode.%s/problems/%s/"):format(
222 config.domain,
223 p.stat.question__title_slug
224 ),
225 paid_only = p.paid_only,
226 ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
227 difficulty = utils.lvl_to_name(p.difficulty.level),
228 starred = p.is_favor,
229 topic_tags = {},
230 }
231 end, problems)
232end
233
234---@param problems lc.cache.Question[]
235---@param titles { questionId: integer, title: string }[]
236function utils.translate_titles(problems, titles)
237 local map = {}
238 for _, title in ipairs(titles) do
239 map[title.questionId] = title.title
240 end
241
242 return vim.tbl_map(function(p)
243 local title = map[tostring(p.id)]
244 if title then
245 p.title_cn = title
246 end
247 return p
248 end, problems)
249end
250
251return utils