• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dangernoodle-io / breadboard / 24967482097

26 Apr 2026 09:28PM UTC coverage: 96.429% (-3.6%) from 100.0%
24967482097

Pull #96

github

web-flow
Merge 810b4efb5 into 9720f4e66
Pull Request #96: feat(bb_openapi): runtime + host emitter for openapi 3.1 spec

215 of 230 branches covered (93.48%)

Branch coverage included in aggregate %.

137 of 140 new or added lines in 1 file covered. (97.86%)

271 of 274 relevant lines covered (98.91%)

183.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.45
/components/bb_openapi/src/bb_openapi_emit.c
1
#include "bb_openapi.h"
2
#include "bb_log.h"
3

4
#include <string.h>
5
#include <ctype.h>
6
#include <stdio.h>
7

8
static const char *TAG = "bb_openapi";
9

10
#define UNIQUE_PATH_CAP 64
11

12
// ---------------------------------------------------------------------------
13
// Method name helpers
14
// ---------------------------------------------------------------------------
15

16
static const char *method_str(bb_http_method_t m)
87✔
17
{
18
    switch (m) {
87✔
19
        case BB_HTTP_GET:     return "get";
58✔
20
        case BB_HTTP_POST:    return "post";
23✔
21
        case BB_HTTP_PATCH:   return "patch";
1✔
22
        case BB_HTTP_PUT:     return "put";
1✔
23
        case BB_HTTP_DELETE:  return "delete";
2✔
24
        case BB_HTTP_OPTIONS: return "options";
1✔
25
        default:              return "get";
1✔
26
    }
27
}
28

29
// ---------------------------------------------------------------------------
30
// operationId derivation
31
// ---------------------------------------------------------------------------
32
// Rule: <method><PathCamelCase>
33
//   - strip leading '/'
34
//   - each path segment's first char is uppercased
35
//   - '-' and '_' are dropped and next char is uppercased
36
//   e.g. GET /api/stats -> "getApiStats"
37
//        POST /api/pool-config -> "postApiPoolConfig"
38

39
static void derive_operation_id(bb_http_method_t method, const char *path,
18✔
40
                                char *out, size_t out_size)
41
{
42
    if (!path || out_size == 0) {
18!
NEW
43
        if (out && out_size > 0) out[0] = '\0';
×
NEW
44
        return;
×
45
    }
46

47
    const char *m = method_str(method);
18✔
48
    size_t pos = 0;
18✔
49

50
    // copy method prefix (already lowercase)
51
    for (const char *p = m; *p && pos < out_size - 1; p++) {
73!
52
        out[pos++] = *p;
55✔
53
    }
54

55
    bool next_upper = true;  // first path char after '/' gets uppercased
18✔
56
    bool skip_slash = true;  // skip the leading '/'
18✔
57

58
    for (const char *p = path; *p && pos < out_size - 1; p++) {
187!
59
        char c = *p;
169✔
60
        if (c == '/') {
169✔
61
            if (skip_slash) {
38✔
62
                skip_slash = false;
18✔
63
            } else {
64
                next_upper = true;
20✔
65
            }
66
            continue;
38✔
67
        }
68
        if (c == '-' || c == '_') {
131✔
69
            next_upper = true;
4✔
70
            continue;
4✔
71
        }
72
        if (next_upper) {
127✔
73
            out[pos++] = (char)toupper((unsigned char)c);
41✔
74
            next_upper = false;
41✔
75
        } else {
76
            out[pos++] = c;
86✔
77
        }
78
    }
79

80
    out[pos] = '\0';
18✔
81
}
82

83
// ---------------------------------------------------------------------------
84
// Path uniqueness tracking (stack array, no malloc)
85
// ---------------------------------------------------------------------------
86

87
typedef struct {
88
    const char *paths[UNIQUE_PATH_CAP];
89
    size_t      count;
90
} path_set_t;
91

92
static bool path_set_contains(const path_set_t *ps, const char *path)
72✔
93
{
94
    for (size_t i = 0; i < ps->count; i++) {
114✔
95
        if (strcmp(ps->paths[i], path) == 0) return true;
43✔
96
    }
97
    return false;
71✔
98
}
99

100
static void path_set_add(path_set_t *ps, const char *path)
72✔
101
{
102
    if (ps->count < UNIQUE_PATH_CAP && !path_set_contains(ps, path)) {
72!
103
        ps->paths[ps->count++] = path;
71✔
104
    }
105
}
72✔
106

107
// ---------------------------------------------------------------------------
108
// Walker context for two-pass emission
109
// ---------------------------------------------------------------------------
110

111
typedef struct {
112
    path_set_t          *path_set;
113
    bb_json_t            paths_obj;
114
    const char          *current_path;
115
    bb_json_t            path_item_obj;
116
} emit_ctx_t;
117

118
// Pass 1: collect unique paths
119
static void collect_paths_walker(const bb_route_t *route, void *ctx)
72✔
120
{
121
    path_set_t *ps = (path_set_t *)ctx;
72✔
122
    if (!route || !route->path) return;
72!
123
    path_set_add(ps, route->path);
72✔
124
}
125

126
// ---------------------------------------------------------------------------
127
// Build a single operation object for a route
128
// ---------------------------------------------------------------------------
129

130
static bb_json_t build_operation(const bb_route_t *route)
70✔
131
{
132
    bb_json_t op = bb_json_obj_new();
70✔
133
    if (!op) return NULL;
70✔
134

135
    // operationId
136
    if (route->operation_id) {
69✔
137
        bb_json_obj_set_string(op, "operationId", route->operation_id);
51✔
138
    } else {
139
        char op_id[128];
140
        derive_operation_id(route->method, route->path, op_id, sizeof(op_id));
18✔
141
        bb_json_obj_set_string(op, "operationId", op_id);
18✔
142
    }
143

144
    // summary
145
    if (route->summary) {
69✔
146
        bb_json_obj_set_string(op, "summary", route->summary);
57✔
147
    }
148

149
    // tags (single-element array)
150
    if (route->tag) {
69✔
151
        bb_json_t tags = bb_json_arr_new();
45✔
152
        if (tags) {
45✔
153
            bb_json_arr_append_string(tags, route->tag);
44✔
154
            bb_json_obj_set_arr(op, "tags", tags);
44✔
155
        }
156
    }
157

158
    // requestBody
159
    if (route->request_schema || route->request_content_type) {
69✔
160
        bb_json_t req_body = bb_json_obj_new();
20✔
161
        bb_json_t content  = bb_json_obj_new();
20✔
162
        bb_json_t media    = bb_json_obj_new();
20✔
163

164
        if (req_body && content && media) {
37✔
165
            if (route->request_schema) {
17✔
166
                bb_json_obj_set_raw(media, "schema", route->request_schema);
16✔
167
            }
168
            const char *ct = route->request_content_type
34✔
169
                             ? route->request_content_type
170
                             : "application/json";
17✔
171
            bb_json_obj_set_obj(content, ct, media);
17✔
172
            bb_json_obj_set_obj(req_body, "content", content);
17✔
173
            bb_json_obj_set_bool(req_body, "required", true);
17✔
174
            bb_json_obj_set_obj(op, "requestBody", req_body);
17✔
175
        } else {
176
            if (req_body) bb_json_free(req_body);
3✔
177
            if (content)  bb_json_free(content);
3✔
178
            if (media)    bb_json_free(media);
3✔
179
        }
180
    }
181

182
    // responses
183
    bb_json_t responses = bb_json_obj_new();
69✔
184
    if (responses && route->responses) {
69✔
185
        for (const bb_route_response_t *r = route->responses; r->status != 0; r++) {
135✔
186
            char status_key[8];
187
            snprintf(status_key, sizeof(status_key), "%d", r->status);
68✔
188

189
            bb_json_t resp_obj = bb_json_obj_new();
68✔
190
            if (!resp_obj) continue;
68✔
191

192
            if (r->description) {
67!
193
                bb_json_obj_set_string(resp_obj, "description", r->description);
67✔
194
            } else {
NEW
195
                bb_json_obj_set_string(resp_obj, "description", "");
×
196
            }
197

198
            if (r->schema && r->content_type) {
67!
199
                bb_json_t content = bb_json_obj_new();
31✔
200
                bb_json_t media   = bb_json_obj_new();
31✔
201
                if (content && media) {
31✔
202
                    bb_json_obj_set_raw(media, "schema", r->schema);
29✔
203
                    bb_json_obj_set_obj(content, r->content_type, media);
29✔
204
                    bb_json_obj_set_obj(resp_obj, "content", content);
29✔
205
                } else {
206
                    if (content) bb_json_free(content);
2✔
207
                    if (media)   bb_json_free(media);
2✔
208
                }
209
            }
210

211
            bb_json_obj_set_obj(responses, status_key, resp_obj);
67✔
212
        }
213
    }
214

215
    if (responses) {
69✔
216
        bb_json_obj_set_obj(op, "responses", responses);
68✔
217
    }
218

219
    return op;
69✔
220
}
221

222
// Pass 2 context: emit operations for a specific path
223
typedef struct {
224
    const char *path;
225
    bb_json_t   path_item;
226
} pass2_ctx_t;
227

228
static void emit_operations_walker(const bb_route_t *route, void *ctx)
154✔
229
{
230
    pass2_ctx_t *p2 = (pass2_ctx_t *)ctx;
154✔
231
    if (!route || !route->path) return;
154!
232
    if (strcmp(route->path, p2->path) != 0) return;
154✔
233

234
    bb_json_t op = build_operation(route);
70✔
235
    if (!op) return;
70✔
236

237
    bb_json_obj_set_obj(p2->path_item, method_str(route->method), op);
69✔
238
}
239

240
// ---------------------------------------------------------------------------
241
// Public emitter
242
// ---------------------------------------------------------------------------
243

244
bb_json_t bb_openapi_emit(const bb_openapi_meta_t *meta)
46✔
245
{
246
    if (!meta) {
46✔
247
        bb_log_e(TAG, "bb_openapi_emit: meta is NULL");
1✔
248
        return NULL;
1✔
249
    }
250

251
    bb_json_t root = bb_json_obj_new();
45✔
252
    if (!root) return NULL;
45✔
253

254
    // openapi version
255
    bb_json_obj_set_string(root, "openapi", "3.1.0");
44✔
256

257
    // info object
258
    bb_json_t info = bb_json_obj_new();
44✔
259
    if (!info) { bb_json_free(root); return NULL; }
44✔
260
    bb_json_obj_set_string(info, "title",   meta->title   ? meta->title   : "");
43✔
261
    bb_json_obj_set_string(info, "version", meta->version ? meta->version : "0.0.0");
43✔
262
    if (meta->description) {
43✔
263
        bb_json_obj_set_string(info, "description", meta->description);
1✔
264
    }
265
    bb_json_obj_set_obj(root, "info", info);
43✔
266

267
    // servers (optional)
268
    if (meta->server_url) {
43✔
269
        bb_json_t servers  = bb_json_arr_new();
3✔
270
        bb_json_t server_e = bb_json_obj_new();
3✔
271
        if (servers && server_e) {
3✔
272
            bb_json_obj_set_string(server_e, "url", meta->server_url);
1✔
273
            bb_json_arr_append_obj(servers, server_e);
1✔
274
            bb_json_obj_set_arr(root, "servers", servers);
1✔
275
        } else {
276
            if (servers)  bb_json_free(servers);
2✔
277
            if (server_e) bb_json_free(server_e);
2✔
278
        }
279
    }
280

281
    // paths — two-pass
282
    path_set_t ps;
283
    memset(&ps, 0, sizeof(ps));
43✔
284
    bb_http_route_registry_foreach(collect_paths_walker, &ps);
43✔
285

286
    bb_json_t paths_obj = bb_json_obj_new();
43✔
287
    if (!paths_obj) { bb_json_free(root); return NULL; }
43✔
288

289
    for (size_t i = 0; i < ps.count; i++) {
112✔
290
        bb_json_t path_item = bb_json_obj_new();
70✔
291
        if (!path_item) continue;
70✔
292

293
        pass2_ctx_t p2 = { .path = ps.paths[i], .path_item = path_item };
69✔
294
        bb_http_route_registry_foreach(emit_operations_walker, &p2);
69✔
295

296
        bb_json_obj_set_obj(paths_obj, ps.paths[i], path_item);
69✔
297
    }
298

299
    bb_json_obj_set_obj(root, "paths", paths_obj);
42✔
300

301
    return root;
42✔
302
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc