yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
usage_schema.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5
6#include "absl/strings/ascii.h"
7#include "absl/strings/match.h"
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_split.h"
10#include "absl/strings/strip.h"
11
12namespace yaze::cli {
13
14namespace {
15
17
18struct Token {
20 std::string text;
21};
22
23bool IsGroupDelimiter(char c) {
24 return c == '[' || c == ']' || c == '(' || c == ')';
25}
26
27bool IsChoiceChar(char c) {
28 return absl::ascii_isalnum(static_cast<unsigned char>(c)) || c == '_' ||
29 c == '-' || c == '.' || c == '+';
30}
31
32bool IsFlagWord(absl::string_view word) {
33 return word.size() > 1 && word[0] == '-' && word != "--";
34}
35
36// Reads one word, keeping `<...>` placeholders intact even when they contain
37// spaces or `|` ("<code folder|.yaze>").
38std::string ReadWord(absl::string_view usage, size_t& pos) {
39 std::string word;
40 while (pos < usage.size()) {
41 const char c = usage[pos];
42 if (c == '<') {
43 size_t end = usage.find('>', pos);
44 if (end == absl::string_view::npos) {
45 end = usage.size() - 1;
46 }
47 absl::StrAppend(&word, usage.substr(pos, end - pos + 1));
48 pos = end + 1;
49 continue;
50 }
51 if (absl::ascii_isspace(static_cast<unsigned char>(c)) ||
53 break;
54 }
55 // "--sandbox|--mock-rom" alternates flags; "json|text" is one value.
56 if (c == '|' && pos + 1 < usage.size() && usage[pos + 1] == '-') {
57 break;
58 }
59 word.push_back(c);
60 ++pos;
61 }
62 return word;
63}
64
65std::vector<Token> Tokenize(absl::string_view usage) {
66 std::vector<Token> tokens;
67 size_t pos = 0;
68 while (pos < usage.size()) {
69 const char c = usage[pos];
70 if (absl::ascii_isspace(static_cast<unsigned char>(c))) {
71 ++pos;
72 continue;
73 }
74 if (c == '[' || c == '(') {
75 tokens.push_back(
76 {c == '[' ? TokenKind::kOpenOptional : TokenKind::kOpenGroup, ""});
77 ++pos;
78 continue;
79 }
80 if (c == ']' || c == ')') {
81 tokens.push_back({TokenKind::kClose, ""});
82 ++pos;
83 continue;
84 }
85 if (c == '|') {
86 tokens.push_back({TokenKind::kBar, ""});
87 ++pos;
88 continue;
89 }
90
91 tokens.push_back({TokenKind::kWord, ReadWord(usage, pos)});
92 }
93 return tokens;
94}
95
96std::string NormalizeHint(absl::string_view hint) {
97 absl::ConsumeSuffix(&hint, "...");
98 if (hint.size() >= 2 && hint.front() == '<' && hint.back() == '>') {
99 hint = hint.substr(1, hint.size() - 2);
100 }
101 absl::ConsumeSuffix(&hint, "...");
102 return std::string(absl::StripAsciiWhitespace(hint));
103}
104
105std::vector<std::string> ExtractChoices(absl::string_view hint) {
106 if (!absl::StrContains(hint, '|')) {
107 return {};
108 }
109 std::vector<std::string> choices = absl::StrSplit(hint, '|');
110 for (const std::string& choice : choices) {
111 if (choice.empty() ||
112 !std::all_of(choice.begin(), choice.end(), IsChoiceChar)) {
113 return {};
114 }
115 }
116 return choices;
117}
118
119bool IsValidName(absl::string_view name) {
120 return !name.empty() &&
121 absl::ascii_isalnum(static_cast<unsigned char>(name.front())) &&
122 std::all_of(name.begin(), name.end(), [](char c) {
123 return absl::ascii_isalnum(static_cast<unsigned char>(c)) ||
124 c == '_' || c == '-';
125 });
126}
127
128std::string PositionalName(absl::string_view placeholder) {
129 std::string name;
130 for (char c : NormalizeHint(placeholder)) {
131 name.push_back(absl::ascii_isalnum(static_cast<unsigned char>(c)) ? c
132 : '_');
133 }
134 return name;
135}
136
137bool IsValueWord(const Token& token) {
138 return token.kind == TokenKind::kWord && !token.text.empty() &&
139 token.text[0] != '-' && token.text != "...";
140}
141
142} // namespace
143
144std::vector<UsageParameter> ParseUsageParameters(absl::string_view usage) {
145 std::vector<Token> tokens = Tokenize(usage);
146
147 size_t index = 0;
148 if (index < tokens.size() && tokens[index].kind == TokenKind::kWord &&
149 tokens[index].text == "z3ed") {
150 ++index;
151 }
152 if (index < tokens.size() && IsValueWord(tokens[index]) &&
153 tokens[index].text[0] != '<') {
154 ++index;
155 }
156
157 std::vector<UsageParameter> parameters;
158 std::vector<bool> group_is_optional;
159 bool after_bar = false;
160
161 auto add = [&](UsageParameter parameter) {
162 parameter.required =
163 !after_bar &&
164 std::none_of(group_is_optional.begin(), group_is_optional.end(),
165 [](bool optional) { return optional; });
166 after_bar = false;
167 const bool duplicate = std::any_of(
168 parameters.begin(), parameters.end(), [&](const UsageParameter& p) {
169 return p.name == parameter.name &&
170 p.positional == parameter.positional;
171 });
172 if (!duplicate) {
173 parameters.push_back(std::move(parameter));
174 }
175 };
176
177 for (; index < tokens.size(); ++index) {
178 const Token& token = tokens[index];
179 switch (token.kind) {
180 case TokenKind::kOpenOptional:
181 case TokenKind::kOpenGroup:
182 group_is_optional.push_back(token.kind == TokenKind::kOpenOptional);
183 break;
184 case TokenKind::kClose:
185 if (!group_is_optional.empty()) {
186 group_is_optional.pop_back();
187 }
188 break;
189 case TokenKind::kBar:
190 if (!parameters.empty()) {
191 parameters.back().required = false;
192 }
193 after_bar = true;
194 break;
195 case TokenKind::kWord: {
196 absl::string_view word = token.text;
197 if (IsFlagWord(word)) {
198 if (!absl::ConsumePrefix(&word, "--")) {
199 absl::ConsumePrefix(&word, "-");
200 }
201 UsageParameter parameter;
202 const size_t equals = word.find('=');
203 if (equals != absl::string_view::npos) {
204 parameter.takes_value = true;
205 parameter.value_hint = NormalizeHint(word.substr(equals + 1));
206 word = word.substr(0, equals);
207 } else if (index + 1 < tokens.size() &&
208 IsValueWord(tokens[index + 1])) {
209 parameter.takes_value = true;
210 parameter.value_hint = NormalizeHint(tokens[++index].text);
211 }
212 if (!IsValidName(word)) {
213 break;
214 }
215 parameter.name = std::string(word);
216 parameter.choices = ExtractChoices(parameter.value_hint);
217 add(std::move(parameter));
218 } else if (word.front() == '<') {
219 UsageParameter parameter;
220 parameter.name = PositionalName(word);
221 parameter.positional = true;
222 parameter.takes_value = true;
223 if (IsValidName(parameter.name)) {
224 add(std::move(parameter));
225 }
226 }
227 break;
228 }
229 }
230 }
231 return parameters;
232}
233
234nlohmann::ordered_json UsageParametersToJsonSchema(
235 const std::vector<UsageParameter>& parameters) {
236 nlohmann::ordered_json properties = nlohmann::ordered_json::object();
237 nlohmann::ordered_json required = nlohmann::ordered_json::array();
238 for (const UsageParameter& parameter : parameters) {
239 if (parameter.positional) {
240 continue;
241 }
242 nlohmann::ordered_json property;
243 property["type"] = parameter.takes_value ? "string" : "boolean";
244 if (!parameter.choices.empty()) {
245 property["enum"] = parameter.choices;
246 } else if (!parameter.value_hint.empty()) {
247 property["description"] = parameter.value_hint;
248 }
249 properties[parameter.name] = std::move(property);
250 if (parameter.required) {
251 required.push_back(parameter.name);
252 }
253 }
254
255 nlohmann::ordered_json schema;
256 schema["type"] = "object";
257 schema["properties"] = std::move(properties);
258 if (!required.empty()) {
259 schema["required"] = std::move(required);
260 }
261 return schema;
262}
263
264} // namespace yaze::cli
std::string PositionalName(absl::string_view placeholder)
std::vector< Token > Tokenize(absl::string_view usage)
std::vector< std::string > ExtractChoices(absl::string_view hint)
std::string NormalizeHint(absl::string_view hint)
std::string ReadWord(absl::string_view usage, size_t &pos)
nlohmann::ordered_json UsageParametersToJsonSchema(const std::vector< UsageParameter > &parameters)
std::vector< UsageParameter > ParseUsageParameters(absl::string_view usage)
std::vector< std::string > choices
bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p)