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

dangernoodle-io / breadboard / 24967612131

26 Apr 2026 09:34PM UTC coverage: 97.976% (-2.0%) from 100.0%
24967612131

Pull #96

github

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

213 of 222 branches covered (95.95%)

Branch coverage included in aggregate %.

137 of 138 new or added lines in 1 file covered. (99.28%)

271 of 272 relevant lines covered (99.63%)

185.21 hits per line

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

96.23
/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
// Caller guarantees: non-NULL path (registry walkers skip NULL-path routes
40
// in collect_paths_walker / emit_operations_walker) and a non-empty buffer
41
// (only call site uses a fixed 128-byte stack array).
42
static void derive_operation_id(bb_http_method_t method, const char *path,
18✔
43
                                char *out, size_t out_size)
44
{
45
    const char *m = method_str(method);
18✔
46
    size_t pos = 0;
18✔
47

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

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

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

78
    out[pos] = '\0';
18✔
79
}
18✔
80

81
// ---------------------------------------------------------------------------
82
// Path uniqueness tracking (stack array, no malloc)
83
// ---------------------------------------------------------------------------
84

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

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

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

105
// ---------------------------------------------------------------------------
106
// Walker context for two-pass emission
107
// ---------------------------------------------------------------------------
108

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

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

124
// ---------------------------------------------------------------------------
125
// Build a single operation object for a route
126
// ---------------------------------------------------------------------------
127

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

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

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

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

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

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

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

187
            bb_json_t resp_obj = bb_json_obj_new();
68✔
188
            if (!resp_obj) continue;
68✔
189

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

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

209
            bb_json_obj_set_obj(responses, status_key, resp_obj);
67✔
210
        }
211
    }
212

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

217
    return op;
69✔
218
}
219

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

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

232
    bb_json_t op = build_operation(route);
70✔
233
    if (!op) return;
70✔
234

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

238
// ---------------------------------------------------------------------------
239
// Public emitter
240
// ---------------------------------------------------------------------------
241

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

249
    bb_json_t root = bb_json_obj_new();
45✔
250
    if (!root) return NULL;
45✔
251

252
    // openapi version
253
    bb_json_obj_set_string(root, "openapi", "3.1.0");
44✔
254

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

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

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

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

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

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

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

297
    bb_json_obj_set_obj(root, "paths", paths_obj);
42✔
298

299
    return root;
42✔
300
}
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