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

dangernoodle-io / breadboard / 25390527202

05 May 2026 05:02PM UTC coverage: 96.769% (-3.2%) from 100.0%
25390527202

Pull #211

github

web-flow
Merge 1d19c329d into 70512697d
Pull Request #211: feat(bb_openapi): public bb_openapi_validate JSON Schema validator

474 of 510 branches covered (92.94%)

Branch coverage included in aggregate %.

145 of 149 new or added lines in 1 file covered. (97.32%)

724 of 728 relevant lines covered (99.45%)

517.22 hits per line

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

86.35
/components/bb_openapi/src/bb_openapi_validate.c
1
#include "bb_openapi.h"
2
#include "bb_log.h"
3

4
#include <cJSON.h>
5
#include <string.h>
6
#include <stdio.h>
7
#include <stdbool.h>
8

9
static const char *TAG = "bb_openapi_validate";
10

11
// ---------------------------------------------------------------------------
12
// Path helpers
13
// ---------------------------------------------------------------------------
14
// Maximum path depth supported by the path stack.
15
#define PATH_DEPTH_MAX 16
16

17
typedef struct {
18
    char segments[PATH_DEPTH_MAX][64];
19
    int  depth;
20
} path_stack_t;
21

22
// Render the current path stack into buf as a dotted string.
23
// Empty (depth == 0) yields "".
24
static void path_render(const path_stack_t *ps, char *buf, size_t bufsz)
12✔
25
{
26
    buf[0] = '\0';
12✔
27
    size_t written = 0;
12✔
28
    for (int i = 0; i < ps->depth && written < bufsz - 1; i++) {
17!
29
        if (i > 0) {
5✔
30
            buf[written++] = '.';
1✔
31
            buf[written] = '\0';
1✔
32
        }
33
        size_t seg_len = strlen(ps->segments[i]);
5✔
34
        size_t avail = bufsz - written - 1;
5✔
35
        if (seg_len > avail) seg_len = avail;
5!
36
        memcpy(buf + written, ps->segments[i], seg_len);
5✔
37
        written += seg_len;
5✔
38
        buf[written] = '\0';
5✔
39
    }
40
}
12✔
41

42
static void path_push(path_stack_t *ps, const char *seg)
15✔
43
{
44
    if (ps->depth >= PATH_DEPTH_MAX) return;
15!
45
    strncpy(ps->segments[ps->depth], seg, sizeof(ps->segments[0]) - 1);
15✔
46
    ps->segments[ps->depth][sizeof(ps->segments[0]) - 1] = '\0';
15✔
47
    ps->depth++;
15✔
48
}
49

50
static void path_push_index(path_stack_t *ps, int idx)
8✔
51
{
52
    if (ps->depth >= PATH_DEPTH_MAX) return;
8!
53
    snprintf(ps->segments[ps->depth], sizeof(ps->segments[0]), "[%d]", idx);
8✔
54
    ps->depth++;
8✔
55
}
56

57
static void path_pop(path_stack_t *ps)
23✔
58
{
59
    if (ps->depth > 0) ps->depth--;
23!
60
}
23✔
61

62
// ---------------------------------------------------------------------------
63
// Type check helpers
64
// ---------------------------------------------------------------------------
65

66
static bool value_matches_type(const cJSON *value, const char *type)
52✔
67
{
68
    if (!type) return true;
52!
69

70
    if (strcmp(type, "string") == 0)  return cJSON_IsString(value);
52✔
71
    if (strcmp(type, "integer") == 0) return cJSON_IsNumber(value);
35✔
72
    if (strcmp(type, "number") == 0)  return cJSON_IsNumber(value);
27!
73
    if (strcmp(type, "boolean") == 0) return cJSON_IsBool(value);
27✔
74
    if (strcmp(type, "null") == 0)    return cJSON_IsNull(value);
23!
75
    if (strcmp(type, "object") == 0)  return cJSON_IsObject(value);
23✔
76
    if (strcmp(type, "array") == 0)   return cJSON_IsArray(value);
6!
77

NEW
78
    bb_log_w(TAG, "unknown JSON Schema type value '%s' — ignored", type);
×
NEW
79
    return true;
×
80
}
81

82
// ---------------------------------------------------------------------------
83
// Forward declaration for recursion
84
// ---------------------------------------------------------------------------
85

86
static bb_err_t validate_node(const cJSON *schema, const cJSON *value,
87
                              path_stack_t *ps,
88
                              bb_openapi_validate_err_t *err);
89

90
// ---------------------------------------------------------------------------
91
// Keyword handlers
92
// ---------------------------------------------------------------------------
93

94
static bb_err_t check_type(const cJSON *schema, const cJSON *value,
52✔
95
                           path_stack_t *ps, bb_openapi_validate_err_t *err)
96
{
97
    cJSON *type_node = cJSON_GetObjectItemCaseSensitive(schema, "type");
52✔
98
    if (!type_node) return BB_OK;
52!
99
    if (!cJSON_IsString(type_node)) return BB_OK;  // malformed keyword — ignore
52!
100

101
    const char *type = type_node->valuestring;
52✔
102
    if (!value_matches_type(value, type)) {
52✔
103
        if (err) {
9✔
104
            path_render(ps, err->path, sizeof(err->path));
8✔
105
            snprintf(err->message, sizeof(err->message),
8✔
106
                     "expected type '%s' but got cJSON type %d", type, value->type);
8✔
107
        }
108
        return BB_ERR_VALIDATION;
9✔
109
    }
110
    return BB_OK;
43✔
111
}
112

113
static bb_err_t check_enum(const cJSON *schema, const cJSON *value,
43✔
114
                           path_stack_t *ps, bb_openapi_validate_err_t *err)
115
{
116
    cJSON *enum_node = cJSON_GetObjectItemCaseSensitive(schema, "enum");
43✔
117
    if (!enum_node) return BB_OK;
43✔
118
    if (!cJSON_IsArray(enum_node)) return BB_OK;
3!
119

120
    const cJSON *entry;
121
    cJSON_ArrayForEach(entry, enum_node) {
16!
122
        if (cJSON_Compare(value, entry, true)) return BB_OK;
15✔
123
    }
124

125
    if (err) {
1!
126
        path_render(ps, err->path, sizeof(err->path));
1✔
127
        char val_str[64] = "<non-string>";
1✔
128
        if (cJSON_IsString(value)) {
1!
129
            strncpy(val_str, value->valuestring, sizeof(val_str) - 1);
1✔
130
            val_str[sizeof(val_str) - 1] = '\0';
1✔
NEW
131
        } else if (cJSON_IsNumber(value)) {
×
NEW
132
            snprintf(val_str, sizeof(val_str), "%g", value->valuedouble);
×
133
        }
134
        snprintf(err->message, sizeof(err->message),
1✔
135
                 "value '%s' is not in enum", val_str);
136
    }
137
    return BB_ERR_VALIDATION;
1✔
138
}
139

140
static bb_err_t check_required(const cJSON *schema, const cJSON *value,
42✔
141
                               path_stack_t *ps, bb_openapi_validate_err_t *err)
142
{
143
    cJSON *req_node = cJSON_GetObjectItemCaseSensitive(schema, "required");
42✔
144
    if (!req_node) return BB_OK;
42✔
145
    if (!cJSON_IsArray(req_node)) return BB_OK;
8!
146
    if (!cJSON_IsObject(value)) return BB_OK;  // type check handles this separately
8!
147

148
    const cJSON *req_key;
149
    cJSON_ArrayForEach(req_key, req_node) {
16!
150
        if (!cJSON_IsString(req_key)) continue;
10!
151
        const char *key_name = req_key->valuestring;
10✔
152
        if (!cJSON_HasObjectItem(value, key_name)) {
10✔
153
            if (err) {
2!
154
                path_render(ps, err->path, sizeof(err->path));
2✔
155
                snprintf(err->message, sizeof(err->message),
2✔
156
                         "required property '%s' is missing", key_name);
157
            }
158
            return BB_ERR_VALIDATION;
2✔
159
        }
160
    }
161
    return BB_OK;
6✔
162
}
163

164
static bb_err_t check_properties(const cJSON *schema, const cJSON *value,
40✔
165
                                 path_stack_t *ps, bb_openapi_validate_err_t *err)
166
{
167
    cJSON *props = cJSON_GetObjectItemCaseSensitive(schema, "properties");
40✔
168
    if (!props) return BB_OK;
40✔
169
    if (!cJSON_IsObject(props)) return BB_OK;
11!
170
    if (!cJSON_IsObject(value)) return BB_OK;
11!
171

172
    // Check additionalProperties: false
173
    cJSON *add_props = cJSON_GetObjectItemCaseSensitive(schema, "additionalProperties");
11✔
174
    bool reject_extra = add_props && cJSON_IsBool(add_props) && !cJSON_IsTrue(add_props);
11!
175

176
    if (reject_extra) {
11✔
177
        const cJSON *val_key;
178
        cJSON_ArrayForEach(val_key, value) {
4!
179
            if (!cJSON_GetObjectItemCaseSensitive(props, val_key->string)) {
3✔
180
                if (err) {
1!
181
                    path_render(ps, err->path, sizeof(err->path));
1✔
182
                    snprintf(err->message, sizeof(err->message),
1✔
183
                             "additional property '%s' not allowed", val_key->string);
1✔
184
                }
185
                return BB_ERR_VALIDATION;
1✔
186
            }
187
        }
188
    }
189

190
    // Recurse into declared properties that exist in the value
191
    const cJSON *prop_schema;
192
    cJSON_ArrayForEach(prop_schema, props) {
28!
193
        const char *key = prop_schema->string;
21✔
194
        cJSON *val_child = cJSON_GetObjectItemCaseSensitive(value, key);
21✔
195
        if (!val_child) continue;  // missing required keys handled by check_required
21✔
196

197
        path_push(ps, key);
15✔
198
        bb_err_t rc = validate_node(prop_schema, val_child, ps, err);
15✔
199
        path_pop(ps);
15✔
200
        if (rc != BB_OK) return rc;
15✔
201
    }
202
    return BB_OK;
7✔
203
}
204

205
static bb_err_t check_items(const cJSON *schema, const cJSON *value,
36✔
206
                            path_stack_t *ps, bb_openapi_validate_err_t *err)
207
{
208
    cJSON *items_schema = cJSON_GetObjectItemCaseSensitive(schema, "items");
36✔
209
    if (!items_schema) return BB_OK;
36✔
210
    if (!cJSON_IsArray(value)) return BB_OK;
4!
211

212
    int idx = 0;
4✔
213
    const cJSON *elem;
214
    cJSON_ArrayForEach(elem, value) {
10!
215
        path_push_index(ps, idx);
8✔
216
        bb_err_t rc = validate_node(items_schema, elem, ps, err);
8✔
217
        path_pop(ps);
8✔
218
        if (rc != BB_OK) return rc;
8✔
219
        idx++;
6✔
220
    }
221
    return BB_OK;
2✔
222
}
223

224
// ---------------------------------------------------------------------------
225
// Unknown keyword warning
226
// ---------------------------------------------------------------------------
227

228
// Known keywords — anything else is warned once.
229
static const char *s_known_keywords[] = {
230
    "type", "properties", "required", "items", "enum", "additionalProperties",
231
    NULL
232
};
233

234
static bool is_known_keyword(const char *key)
81✔
235
{
236
    for (int i = 0; s_known_keywords[i]; i++) {
148✔
237
        if (strcmp(key, s_known_keywords[i]) == 0) return true;
147✔
238
    }
239
    return false;
1✔
240
}
241

242
static void warn_unknown_keywords(const cJSON *schema)
52✔
243
{
244
    const cJSON *kw;
245
    cJSON_ArrayForEach(kw, schema) {
133!
246
        if (kw->string && !is_known_keyword(kw->string)) {
81!
247
            bb_log_w(TAG, "unknown JSON Schema keyword '%s' — ignored", kw->string);
1✔
248
        }
249
    }
250
}
52✔
251

252
// ---------------------------------------------------------------------------
253
// Core recursive validator
254
// ---------------------------------------------------------------------------
255

256
static bb_err_t validate_node(const cJSON *schema, const cJSON *value,
52✔
257
                              path_stack_t *ps,
258
                              bb_openapi_validate_err_t *err)
259
{
260
    if (!schema || !value) return BB_OK;
52!
261
    if (!cJSON_IsObject(schema)) return BB_OK;
52!
262

263
    warn_unknown_keywords(schema);
52✔
264

265
    bb_err_t rc;
266

267
    rc = check_type(schema, value, ps, err);
52✔
268
    if (rc != BB_OK) return rc;
52✔
269

270
    rc = check_enum(schema, value, ps, err);
43✔
271
    if (rc != BB_OK) return rc;
43✔
272

273
    rc = check_required(schema, value, ps, err);
42✔
274
    if (rc != BB_OK) return rc;
42✔
275

276
    rc = check_properties(schema, value, ps, err);
40✔
277
    if (rc != BB_OK) return rc;
40✔
278

279
    rc = check_items(schema, value, ps, err);
36✔
280
    if (rc != BB_OK) return rc;
36✔
281

282
    return BB_OK;
34✔
283
}
284

285
// ---------------------------------------------------------------------------
286
// Public API
287
// ---------------------------------------------------------------------------
288

289
bb_err_t bb_openapi_validate(const char *schema_json,
32✔
290
                             const cJSON *value,
291
                             bb_openapi_validate_err_t *err)
292
{
293
    if (!schema_json || !value) return BB_ERR_INVALID_ARG;
32✔
294

295
    cJSON *schema = cJSON_Parse(schema_json);
30✔
296
    if (!schema) {
30✔
297
        bb_log_e(TAG, "schema_json failed to parse as JSON");
1✔
298
        return BB_ERR_INVALID_ARG;
1✔
299
    }
300

301
    path_stack_t ps;
302
    memset(&ps, 0, sizeof(ps));
29✔
303

304
    bb_err_t rc = validate_node(schema, value, &ps, err);
29✔
305

306
    cJSON_Delete(schema);
29✔
307
    return rc;
29✔
308
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc