Fossil SCM
Add artifact_to_json() support.
Commit
1e2d60287a002c69812d6ad2f03fec0df1f68d98c1aff89d247b9fb685a0d378
Parent
c591bbe0adc3f7b…
2 files changed
+2
+294
M
src/db.c
+2
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -1629,10 +1629,12 @@ | ||
| 1629 | 1629 | sqlite3_create_function(db, "chat_msg_from_event", 4, |
| 1630 | 1630 | SQLITE_UTF8 | SQLITE_INNOCUOUS, 0, |
| 1631 | 1631 | chat_msg_from_event, 0, 0); |
| 1632 | 1632 | sqlite3_create_function(db, "inode", 1, SQLITE_UTF8, 0, |
| 1633 | 1633 | file_inode_sql_func,0,0); |
| 1634 | + sqlite3_create_function(db, "artifact_to_json", 1, SQLITE_UTF8, 0, | |
| 1635 | + artifact_to_json_sql_func,0,0); | |
| 1634 | 1636 | |
| 1635 | 1637 | } |
| 1636 | 1638 | |
| 1637 | 1639 | #if USE_SEE |
| 1638 | 1640 | /* |
| 1639 | 1641 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -1629,10 +1629,12 @@ | |
| 1629 | sqlite3_create_function(db, "chat_msg_from_event", 4, |
| 1630 | SQLITE_UTF8 | SQLITE_INNOCUOUS, 0, |
| 1631 | chat_msg_from_event, 0, 0); |
| 1632 | sqlite3_create_function(db, "inode", 1, SQLITE_UTF8, 0, |
| 1633 | file_inode_sql_func,0,0); |
| 1634 | |
| 1635 | } |
| 1636 | |
| 1637 | #if USE_SEE |
| 1638 | /* |
| 1639 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -1629,10 +1629,12 @@ | |
| 1629 | sqlite3_create_function(db, "chat_msg_from_event", 4, |
| 1630 | SQLITE_UTF8 | SQLITE_INNOCUOUS, 0, |
| 1631 | chat_msg_from_event, 0, 0); |
| 1632 | sqlite3_create_function(db, "inode", 1, SQLITE_UTF8, 0, |
| 1633 | file_inode_sql_func,0,0); |
| 1634 | sqlite3_create_function(db, "artifact_to_json", 1, SQLITE_UTF8, 0, |
| 1635 | artifact_to_json_sql_func,0,0); |
| 1636 | |
| 1637 | } |
| 1638 | |
| 1639 | #if USE_SEE |
| 1640 | /* |
| 1641 |
+294
| --- src/manifest.c | ||
| +++ src/manifest.c | ||
| @@ -2911,5 +2911,299 @@ | ||
| 2911 | 2911 | if( g.argc!=3 ) usage("RECORDID"); |
| 2912 | 2912 | rid = name_to_rid(g.argv[2]); |
| 2913 | 2913 | content_get(rid, &content); |
| 2914 | 2914 | manifest_crosslink(rid, &content, MC_NONE); |
| 2915 | 2915 | } |
| 2916 | + | |
| 2917 | +/* | |
| 2918 | +** For a given CATYPE_... value, returns a human-friendly name, or | |
| 2919 | +** NULL if typeId is unknown or is CFTYPE_ANY. | |
| 2920 | +*/ | |
| 2921 | +const char * artifact_type_to_name(int typeId){ | |
| 2922 | + switch(typeId){ | |
| 2923 | + case CFTYPE_MANIFEST: return "checkin"; | |
| 2924 | + case CFTYPE_CLUSTER: return "cluster"; | |
| 2925 | + case CFTYPE_CONTROL: return "tag"; | |
| 2926 | + case CFTYPE_WIKI: return "wiki"; | |
| 2927 | + case CFTYPE_TICKET: return "ticket"; | |
| 2928 | + case CFTYPE_ATTACHMENT: return "attachment"; | |
| 2929 | + case CFTYPE_EVENT: return "event"; | |
| 2930 | + case CFTYPE_FORUM: return "forumpost"; | |
| 2931 | + } | |
| 2932 | + return NULL; | |
| 2933 | +} | |
| 2934 | + | |
| 2935 | +/* | |
| 2936 | +** Creates a JSON representation of p, appending it to b. | |
| 2937 | +** | |
| 2938 | +** b is not cleared before rendering, so the caller needs to do that | |
| 2939 | +** if it's important for their use case. | |
| 2940 | +** | |
| 2941 | +** Pedantic note: this routine traverses p->aFile directly, rather than | |
| 2942 | +** using manifest_file_next(), so that delta manifests are rendered as-is | |
| 2943 | +** instead of having their derived files. If that policy is ever changed, | |
| 2944 | +** p will need to be non-const. | |
| 2945 | +*/ | |
| 2946 | +void artifact_to_json(Manifest const *p, Blob *b){ | |
| 2947 | + int i; | |
| 2948 | + char *zUuid; | |
| 2949 | + | |
| 2950 | + blob_append_literal(b, "{"); | |
| 2951 | + zUuid = rid_to_uuid(p->rid); | |
| 2952 | + blob_appendf(b, "\"uuid\": %!j", zUuid); | |
| 2953 | + /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ | |
| 2954 | + blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type)); | |
| 2955 | +#define ISA(TYPE) if( p->type==TYPE ) | |
| 2956 | +#define CARD_LETTER(LETTER) \ | |
| 2957 | + blob_append_literal(b, ",\"" #LETTER "\": ") | |
| 2958 | +#define CARD_STR(LETTER, VAL) \ | |
| 2959 | + assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) | |
| 2960 | +#define CARD_STR2(LETTER, VAL) \ | |
| 2961 | + if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 | |
| 2962 | +#define STR_OR_NULL(VAL) \ | |
| 2963 | + if( VAL ) blob_appendf(b, "%!j", VAL); \ | |
| 2964 | + else blob_append(b, "null", 4) | |
| 2965 | +#define KVP_STR(ADDCOMMA, KEY,VAL) \ | |
| 2966 | + if(ADDCOMMA) blob_append_char(b, ','); \ | |
| 2967 | + blob_appendf(b, "%!j: ", #KEY); \ | |
| 2968 | + STR_OR_NULL(VAL) | |
| 2969 | + | |
| 2970 | + /* Noting that only 1 (at most) of the A-card pieces will be non-NULL... */ | |
| 2971 | + ISA( CFTYPE_ATTACHMENT ){ | |
| 2972 | + CARD_LETTER(A); | |
| 2973 | + blob_append_char(b, '{'); | |
| 2974 | + KVP_STR(0, filename, p->zAttachName); | |
| 2975 | + KVP_STR(1, target, p->zAttachTarget); | |
| 2976 | + KVP_STR(1, source, p->zAttachSrc); | |
| 2977 | + blob_append_char(b, '}'); | |
| 2978 | + } | |
| 2979 | + CARD_STR2(B, p->zBaseline); | |
| 2980 | + CARD_STR2(C, p->zComment); | |
| 2981 | + CARD_LETTER(D); blob_appendf(b, "%f", p->rDate); | |
| 2982 | + ISA( CFTYPE_EVENT ){ | |
| 2983 | + blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}", | |
| 2984 | + p->rEventDate, p->zEventId); | |
| 2985 | + } | |
| 2986 | + ISA( CFTYPE_MANIFEST ){ | |
| 2987 | + CARD_LETTER(F); | |
| 2988 | + blob_append_char(b, '['); | |
| 2989 | + for( i = 0; i < p->nFile; ++i ){ | |
| 2990 | + ManifestFile const * const pF = &p->aFile[i]; | |
| 2991 | + if( i>0 ) blob_append_char(b, ','); | |
| 2992 | + blob_append_char(b, '{'); | |
| 2993 | + KVP_STR(0, name, pF->zName); | |
| 2994 | + KVP_STR(1, uuid, pF->zUuid); | |
| 2995 | + KVP_STR(1, perm, pF->zPerm); | |
| 2996 | + KVP_STR(1, oldName, pF->zPrior); | |
| 2997 | + blob_append_char(b, '}'); | |
| 2998 | + } | |
| 2999 | + /* Special case: model checkins with no F-card as having an empty | |
| 3000 | + ** array, rather than no F-cards, to hypothetically simplify | |
| 3001 | + ** handling in JSON queries. */ | |
| 3002 | + blob_append_char(b, ']'); | |
| 3003 | + } | |
| 3004 | + CARD_STR2(G, p->zThreadRoot); | |
| 3005 | + CARD_STR2(H, p->zThreadTitle); | |
| 3006 | + CARD_STR2(I, p->zInReplyTo); | |
| 3007 | + if( p->nField ){ | |
| 3008 | + CARD_LETTER(J); | |
| 3009 | + blob_append_char(b, '['); | |
| 3010 | + for( i = 0; i < p->nField; ++i ){ | |
| 3011 | + if( i>0 ) blob_append_char(b, ','); | |
| 3012 | + blob_append_char(b, '{'); | |
| 3013 | + KVP_STR(0, name, p->aField[i].zName); | |
| 3014 | + KVP_STR(1, value, p->aField[i].zValue); | |
| 3015 | + blob_append_char(b, '}'); | |
| 3016 | + } | |
| 3017 | + blob_append_char(b, ']'); | |
| 3018 | + } | |
| 3019 | + CARD_STR2(K, p->zTicketUuid); | |
| 3020 | + CARD_STR2(L, p->zWikiTitle); | |
| 3021 | + ISA( CFTYPE_CLUSTER && p->nCChild>0 ){ | |
| 3022 | + CARD_LETTER(M); | |
| 3023 | + blob_append_char(b, '['); | |
| 3024 | + for( int i = 0; i < p->nCChild; ++i ){ | |
| 3025 | + if( i>0 ) blob_append_char(b, ','); | |
| 3026 | + blob_appendf(b, "%!j", p->azCChild[i]); | |
| 3027 | + } | |
| 3028 | + blob_append_char(b, ']'); | |
| 3029 | + } | |
| 3030 | + CARD_STR2(N, p->zMimetype); | |
| 3031 | + ISA( CFTYPE_MANIFEST ){ | |
| 3032 | + CARD_LETTER(P); | |
| 3033 | + blob_append_char(b, '['); | |
| 3034 | + if( p->nParent ){ | |
| 3035 | + for( i = 0; i < p->nParent; ++i ){ | |
| 3036 | + if( i>0 ) blob_append_char(b, ','); | |
| 3037 | + blob_appendf(b, "%!j", p->azParent[i]); | |
| 3038 | + } | |
| 3039 | + } | |
| 3040 | + /* Special case: model checkins with no P-card as having | |
| 3041 | + ** an empty array, rather than no P-card, to hypothetically | |
| 3042 | + ** simplify handling in JSON queries. */ | |
| 3043 | + blob_append_char(b, ']'); | |
| 3044 | + } | |
| 3045 | + if( p->nCherrypick ){ | |
| 3046 | + CARD_LETTER(Q); | |
| 3047 | + blob_append_char(b, '['); | |
| 3048 | + for( i = 0; i < p->nCherrypick; ++i ){ | |
| 3049 | + if( i>0 ) blob_append_char(b, ','); | |
| 3050 | + blob_append_char(b, '{'); | |
| 3051 | + blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]); | |
| 3052 | + KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); | |
| 3053 | + KVP_STR(1, base, p->aCherrypick[i].zCPBase); | |
| 3054 | + blob_append_char(b, '}'); | |
| 3055 | + } | |
| 3056 | + blob_append_char(b, ']'); | |
| 3057 | + } | |
| 3058 | + CARD_STR2(R, p->zRepoCksum); | |
| 3059 | + if( p->nTag ){ | |
| 3060 | + CARD_LETTER(T); | |
| 3061 | + blob_append_char(b, '['); | |
| 3062 | + for( int i = 0; i < p->nTag; ++i ){ | |
| 3063 | + const char *zName = p->aTag[i].zName; | |
| 3064 | + if( i>0 ) blob_append_char(b, ','); | |
| 3065 | + blob_append_char(b, '{'); | |
| 3066 | + blob_appendf(b, "\"type\": \"%c\"", *zName); | |
| 3067 | + ++zName; | |
| 3068 | + KVP_STR(1, name, zName); | |
| 3069 | + KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") | |
| 3070 | + /* We could arguably resolve this to null. Per /chat | |
| 3071 | + discussion we don't want to resolve it to zUuid because | |
| 3072 | + that would effectively add information to the rendered | |
| 3073 | + manifest. */; | |
| 3074 | + KVP_STR(1, value, p->aTag[i].zValue); | |
| 3075 | + blob_append_char(b, '}'); | |
| 3076 | + } | |
| 3077 | + blob_append_char(b, ']'); | |
| 3078 | + } | |
| 3079 | + CARD_STR2(U, p->zUser); | |
| 3080 | + CARD_STR2(W, p->zWiki); | |
| 3081 | + fossil_free(zUuid); | |
| 3082 | + blob_append_literal(b, "}"); | |
| 3083 | +#undef CARD_FMT | |
| 3084 | +#undef CARD_LETTER | |
| 3085 | +#undef CARD_STR | |
| 3086 | +#undef CARD_STR2 | |
| 3087 | +#undef ISA | |
| 3088 | +#undef KVP_STR | |
| 3089 | +#undef STR_OR_NULL | |
| 3090 | +} | |
| 3091 | + | |
| 3092 | +/* | |
| 3093 | +** Convenience wrapper around artifact_to_json() which expects rid to | |
| 3094 | +** be the blob.rid of any artifact type. If it can load a Manifest | |
| 3095 | +** with that rid, it returns rid, else it returns 0. | |
| 3096 | +*/ | |
| 3097 | +int artifact_to_json_by_rid(int rid, Blob *pOut){ | |
| 3098 | + Manifest * const p = manifest_get(rid, CFTYPE_ANY, 0); | |
| 3099 | + if( p ){ | |
| 3100 | + artifact_to_json(p, pOut); | |
| 3101 | + manifest_destroy(p); | |
| 3102 | + }else{ | |
| 3103 | + rid = 0; | |
| 3104 | + } | |
| 3105 | + return rid; | |
| 3106 | +} | |
| 3107 | + | |
| 3108 | +/* | |
| 3109 | +** Convenience wrapper around artifact_to_json() which accepts any | |
| 3110 | +** artifact name which is legal for symbolic_name_to_rid(). On success | |
| 3111 | +** it returns the rid of the artifact. Returns 0 if no such artifact | |
| 3112 | +** exists and a negative value if the name is ambiguous. | |
| 3113 | +** | |
| 3114 | +** pOut is not cleared before rendering, so the caller needs to do | |
| 3115 | +** that if it's important for their use case. | |
| 3116 | +*/ | |
| 3117 | +int artifact_to_json_by_name(const char *zName, Blob *pOut){ | |
| 3118 | + const int rid = symbolic_name_to_rid(zName, 0); | |
| 3119 | + return rid>0 | |
| 3120 | + ? artifact_to_json_by_rid(rid, pOut) | |
| 3121 | + : rid; | |
| 3122 | +} | |
| 3123 | + | |
| 3124 | +/* | |
| 3125 | +** SQLite UDF for artifact_to_json(). Its single argument should be | |
| 3126 | +** either an INTEGER (blob.rid value) or a TEXT symbolic artifact | |
| 3127 | +** name, as per symbolic_name_to_rid(). If an artifact is found then | |
| 3128 | +** the result of the UDF is that JSON as a string, else it evaluates | |
| 3129 | +** to NULL. | |
| 3130 | +*/ | |
| 3131 | +void artifact_to_json_sql_func( | |
| 3132 | + sqlite3_context *context, | |
| 3133 | + int argc, | |
| 3134 | + sqlite3_value **argv | |
| 3135 | +){ | |
| 3136 | + int rid = 0; | |
| 3137 | + Blob b = empty_blob; | |
| 3138 | + | |
| 3139 | + if(1 != argc){ | |
| 3140 | + goto error_usage; | |
| 3141 | + } | |
| 3142 | + switch( sqlite3_value_type(argv[0]) ){ | |
| 3143 | + case SQLITE_INTEGER: | |
| 3144 | + rid = artifact_to_json_by_rid(sqlite3_value_int(argv[0]), &b); | |
| 3145 | + break; | |
| 3146 | + case SQLITE_TEXT:{ | |
| 3147 | + const char * z = (const char *)sqlite3_value_text(argv[0]); | |
| 3148 | + if( z ){ | |
| 3149 | + rid = artifact_to_json_by_name(z, &b); | |
| 3150 | + } | |
| 3151 | + break; | |
| 3152 | + } | |
| 3153 | + default: | |
| 3154 | + goto error_usage; | |
| 3155 | + } | |
| 3156 | + if( rid>0 ){ | |
| 3157 | + sqlite3_result_text(context, blob_str(&b), blob_size(&b), | |
| 3158 | + SQLITE_TRANSIENT); | |
| 3159 | + blob_reset(&b); | |
| 3160 | + }else{ | |
| 3161 | + /* We should arguably error out if rid<0 (ambiguous name) */ | |
| 3162 | + sqlite3_result_null(context); | |
| 3163 | + } | |
| 3164 | + return; | |
| 3165 | +error_usage: | |
| 3166 | + sqlite3_result_error(context, "Expecting one argument: blob.rid or " | |
| 3167 | + "artifact symbolic name", -1); | |
| 3168 | +} | |
| 3169 | + | |
| 3170 | + | |
| 3171 | + | |
| 3172 | +/* | |
| 3173 | +** COMMAND: test-artifact-to-json | |
| 3174 | +** | |
| 3175 | +** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names] | |
| 3176 | +** | |
| 3177 | +** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. | |
| 3178 | +*/ | |
| 3179 | +void test_manifest_to_json(void){ | |
| 3180 | + int i; | |
| 3181 | + Blob b = empty_blob; | |
| 3182 | + Stmt q; | |
| 3183 | + const int bPretty = find_option("pretty",0,0)!=0; | |
| 3184 | + int nErr = 0; | |
| 3185 | + | |
| 3186 | + db_find_and_open_repository(0,0); | |
| 3187 | + db_prepare(&q, "select json_pretty(:json)"); | |
| 3188 | + for( i=2; i<g.argc; ++i ){ | |
| 3189 | + char const *zName = g.argv[i]; | |
| 3190 | + const int rc = artifact_to_json_by_name(zName, &b); | |
| 3191 | + if( rc<=0 ){ | |
| 3192 | + ++nErr; | |
| 3193 | + fossil_warning("Error reading artifact %Q", zName); | |
| 3194 | + continue; | |
| 3195 | + }else if( bPretty ){ | |
| 3196 | + db_bind_blob(&q, ":json", &b); | |
| 3197 | + b.nUsed = 0; | |
| 3198 | + db_step(&q); | |
| 3199 | + db_column_blob(&q, 0, &b); | |
| 3200 | + db_reset(&q); | |
| 3201 | + } | |
| 3202 | + fossil_print("%b\n", &b); | |
| 3203 | + blob_reset(&b); | |
| 3204 | + } | |
| 3205 | + db_finalize(&q); | |
| 3206 | + if( nErr ){ | |
| 3207 | + fossil_warning("Error count: %d", nErr); | |
| 3208 | + } | |
| 3209 | +} | |
| 2916 | 3210 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -2911,5 +2911,299 @@ | |
| 2911 | if( g.argc!=3 ) usage("RECORDID"); |
| 2912 | rid = name_to_rid(g.argv[2]); |
| 2913 | content_get(rid, &content); |
| 2914 | manifest_crosslink(rid, &content, MC_NONE); |
| 2915 | } |
| 2916 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -2911,5 +2911,299 @@ | |
| 2911 | if( g.argc!=3 ) usage("RECORDID"); |
| 2912 | rid = name_to_rid(g.argv[2]); |
| 2913 | content_get(rid, &content); |
| 2914 | manifest_crosslink(rid, &content, MC_NONE); |
| 2915 | } |
| 2916 | |
| 2917 | /* |
| 2918 | ** For a given CATYPE_... value, returns a human-friendly name, or |
| 2919 | ** NULL if typeId is unknown or is CFTYPE_ANY. |
| 2920 | */ |
| 2921 | const char * artifact_type_to_name(int typeId){ |
| 2922 | switch(typeId){ |
| 2923 | case CFTYPE_MANIFEST: return "checkin"; |
| 2924 | case CFTYPE_CLUSTER: return "cluster"; |
| 2925 | case CFTYPE_CONTROL: return "tag"; |
| 2926 | case CFTYPE_WIKI: return "wiki"; |
| 2927 | case CFTYPE_TICKET: return "ticket"; |
| 2928 | case CFTYPE_ATTACHMENT: return "attachment"; |
| 2929 | case CFTYPE_EVENT: return "event"; |
| 2930 | case CFTYPE_FORUM: return "forumpost"; |
| 2931 | } |
| 2932 | return NULL; |
| 2933 | } |
| 2934 | |
| 2935 | /* |
| 2936 | ** Creates a JSON representation of p, appending it to b. |
| 2937 | ** |
| 2938 | ** b is not cleared before rendering, so the caller needs to do that |
| 2939 | ** if it's important for their use case. |
| 2940 | ** |
| 2941 | ** Pedantic note: this routine traverses p->aFile directly, rather than |
| 2942 | ** using manifest_file_next(), so that delta manifests are rendered as-is |
| 2943 | ** instead of having their derived files. If that policy is ever changed, |
| 2944 | ** p will need to be non-const. |
| 2945 | */ |
| 2946 | void artifact_to_json(Manifest const *p, Blob *b){ |
| 2947 | int i; |
| 2948 | char *zUuid; |
| 2949 | |
| 2950 | blob_append_literal(b, "{"); |
| 2951 | zUuid = rid_to_uuid(p->rid); |
| 2952 | blob_appendf(b, "\"uuid\": %!j", zUuid); |
| 2953 | /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ |
| 2954 | blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type)); |
| 2955 | #define ISA(TYPE) if( p->type==TYPE ) |
| 2956 | #define CARD_LETTER(LETTER) \ |
| 2957 | blob_append_literal(b, ",\"" #LETTER "\": ") |
| 2958 | #define CARD_STR(LETTER, VAL) \ |
| 2959 | assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) |
| 2960 | #define CARD_STR2(LETTER, VAL) \ |
| 2961 | if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 |
| 2962 | #define STR_OR_NULL(VAL) \ |
| 2963 | if( VAL ) blob_appendf(b, "%!j", VAL); \ |
| 2964 | else blob_append(b, "null", 4) |
| 2965 | #define KVP_STR(ADDCOMMA, KEY,VAL) \ |
| 2966 | if(ADDCOMMA) blob_append_char(b, ','); \ |
| 2967 | blob_appendf(b, "%!j: ", #KEY); \ |
| 2968 | STR_OR_NULL(VAL) |
| 2969 | |
| 2970 | /* Noting that only 1 (at most) of the A-card pieces will be non-NULL... */ |
| 2971 | ISA( CFTYPE_ATTACHMENT ){ |
| 2972 | CARD_LETTER(A); |
| 2973 | blob_append_char(b, '{'); |
| 2974 | KVP_STR(0, filename, p->zAttachName); |
| 2975 | KVP_STR(1, target, p->zAttachTarget); |
| 2976 | KVP_STR(1, source, p->zAttachSrc); |
| 2977 | blob_append_char(b, '}'); |
| 2978 | } |
| 2979 | CARD_STR2(B, p->zBaseline); |
| 2980 | CARD_STR2(C, p->zComment); |
| 2981 | CARD_LETTER(D); blob_appendf(b, "%f", p->rDate); |
| 2982 | ISA( CFTYPE_EVENT ){ |
| 2983 | blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}", |
| 2984 | p->rEventDate, p->zEventId); |
| 2985 | } |
| 2986 | ISA( CFTYPE_MANIFEST ){ |
| 2987 | CARD_LETTER(F); |
| 2988 | blob_append_char(b, '['); |
| 2989 | for( i = 0; i < p->nFile; ++i ){ |
| 2990 | ManifestFile const * const pF = &p->aFile[i]; |
| 2991 | if( i>0 ) blob_append_char(b, ','); |
| 2992 | blob_append_char(b, '{'); |
| 2993 | KVP_STR(0, name, pF->zName); |
| 2994 | KVP_STR(1, uuid, pF->zUuid); |
| 2995 | KVP_STR(1, perm, pF->zPerm); |
| 2996 | KVP_STR(1, oldName, pF->zPrior); |
| 2997 | blob_append_char(b, '}'); |
| 2998 | } |
| 2999 | /* Special case: model checkins with no F-card as having an empty |
| 3000 | ** array, rather than no F-cards, to hypothetically simplify |
| 3001 | ** handling in JSON queries. */ |
| 3002 | blob_append_char(b, ']'); |
| 3003 | } |
| 3004 | CARD_STR2(G, p->zThreadRoot); |
| 3005 | CARD_STR2(H, p->zThreadTitle); |
| 3006 | CARD_STR2(I, p->zInReplyTo); |
| 3007 | if( p->nField ){ |
| 3008 | CARD_LETTER(J); |
| 3009 | blob_append_char(b, '['); |
| 3010 | for( i = 0; i < p->nField; ++i ){ |
| 3011 | if( i>0 ) blob_append_char(b, ','); |
| 3012 | blob_append_char(b, '{'); |
| 3013 | KVP_STR(0, name, p->aField[i].zName); |
| 3014 | KVP_STR(1, value, p->aField[i].zValue); |
| 3015 | blob_append_char(b, '}'); |
| 3016 | } |
| 3017 | blob_append_char(b, ']'); |
| 3018 | } |
| 3019 | CARD_STR2(K, p->zTicketUuid); |
| 3020 | CARD_STR2(L, p->zWikiTitle); |
| 3021 | ISA( CFTYPE_CLUSTER && p->nCChild>0 ){ |
| 3022 | CARD_LETTER(M); |
| 3023 | blob_append_char(b, '['); |
| 3024 | for( int i = 0; i < p->nCChild; ++i ){ |
| 3025 | if( i>0 ) blob_append_char(b, ','); |
| 3026 | blob_appendf(b, "%!j", p->azCChild[i]); |
| 3027 | } |
| 3028 | blob_append_char(b, ']'); |
| 3029 | } |
| 3030 | CARD_STR2(N, p->zMimetype); |
| 3031 | ISA( CFTYPE_MANIFEST ){ |
| 3032 | CARD_LETTER(P); |
| 3033 | blob_append_char(b, '['); |
| 3034 | if( p->nParent ){ |
| 3035 | for( i = 0; i < p->nParent; ++i ){ |
| 3036 | if( i>0 ) blob_append_char(b, ','); |
| 3037 | blob_appendf(b, "%!j", p->azParent[i]); |
| 3038 | } |
| 3039 | } |
| 3040 | /* Special case: model checkins with no P-card as having |
| 3041 | ** an empty array, rather than no P-card, to hypothetically |
| 3042 | ** simplify handling in JSON queries. */ |
| 3043 | blob_append_char(b, ']'); |
| 3044 | } |
| 3045 | if( p->nCherrypick ){ |
| 3046 | CARD_LETTER(Q); |
| 3047 | blob_append_char(b, '['); |
| 3048 | for( i = 0; i < p->nCherrypick; ++i ){ |
| 3049 | if( i>0 ) blob_append_char(b, ','); |
| 3050 | blob_append_char(b, '{'); |
| 3051 | blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]); |
| 3052 | KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); |
| 3053 | KVP_STR(1, base, p->aCherrypick[i].zCPBase); |
| 3054 | blob_append_char(b, '}'); |
| 3055 | } |
| 3056 | blob_append_char(b, ']'); |
| 3057 | } |
| 3058 | CARD_STR2(R, p->zRepoCksum); |
| 3059 | if( p->nTag ){ |
| 3060 | CARD_LETTER(T); |
| 3061 | blob_append_char(b, '['); |
| 3062 | for( int i = 0; i < p->nTag; ++i ){ |
| 3063 | const char *zName = p->aTag[i].zName; |
| 3064 | if( i>0 ) blob_append_char(b, ','); |
| 3065 | blob_append_char(b, '{'); |
| 3066 | blob_appendf(b, "\"type\": \"%c\"", *zName); |
| 3067 | ++zName; |
| 3068 | KVP_STR(1, name, zName); |
| 3069 | KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") |
| 3070 | /* We could arguably resolve this to null. Per /chat |
| 3071 | discussion we don't want to resolve it to zUuid because |
| 3072 | that would effectively add information to the rendered |
| 3073 | manifest. */; |
| 3074 | KVP_STR(1, value, p->aTag[i].zValue); |
| 3075 | blob_append_char(b, '}'); |
| 3076 | } |
| 3077 | blob_append_char(b, ']'); |
| 3078 | } |
| 3079 | CARD_STR2(U, p->zUser); |
| 3080 | CARD_STR2(W, p->zWiki); |
| 3081 | fossil_free(zUuid); |
| 3082 | blob_append_literal(b, "}"); |
| 3083 | #undef CARD_FMT |
| 3084 | #undef CARD_LETTER |
| 3085 | #undef CARD_STR |
| 3086 | #undef CARD_STR2 |
| 3087 | #undef ISA |
| 3088 | #undef KVP_STR |
| 3089 | #undef STR_OR_NULL |
| 3090 | } |
| 3091 | |
| 3092 | /* |
| 3093 | ** Convenience wrapper around artifact_to_json() which expects rid to |
| 3094 | ** be the blob.rid of any artifact type. If it can load a Manifest |
| 3095 | ** with that rid, it returns rid, else it returns 0. |
| 3096 | */ |
| 3097 | int artifact_to_json_by_rid(int rid, Blob *pOut){ |
| 3098 | Manifest * const p = manifest_get(rid, CFTYPE_ANY, 0); |
| 3099 | if( p ){ |
| 3100 | artifact_to_json(p, pOut); |
| 3101 | manifest_destroy(p); |
| 3102 | }else{ |
| 3103 | rid = 0; |
| 3104 | } |
| 3105 | return rid; |
| 3106 | } |
| 3107 | |
| 3108 | /* |
| 3109 | ** Convenience wrapper around artifact_to_json() which accepts any |
| 3110 | ** artifact name which is legal for symbolic_name_to_rid(). On success |
| 3111 | ** it returns the rid of the artifact. Returns 0 if no such artifact |
| 3112 | ** exists and a negative value if the name is ambiguous. |
| 3113 | ** |
| 3114 | ** pOut is not cleared before rendering, so the caller needs to do |
| 3115 | ** that if it's important for their use case. |
| 3116 | */ |
| 3117 | int artifact_to_json_by_name(const char *zName, Blob *pOut){ |
| 3118 | const int rid = symbolic_name_to_rid(zName, 0); |
| 3119 | return rid>0 |
| 3120 | ? artifact_to_json_by_rid(rid, pOut) |
| 3121 | : rid; |
| 3122 | } |
| 3123 | |
| 3124 | /* |
| 3125 | ** SQLite UDF for artifact_to_json(). Its single argument should be |
| 3126 | ** either an INTEGER (blob.rid value) or a TEXT symbolic artifact |
| 3127 | ** name, as per symbolic_name_to_rid(). If an artifact is found then |
| 3128 | ** the result of the UDF is that JSON as a string, else it evaluates |
| 3129 | ** to NULL. |
| 3130 | */ |
| 3131 | void artifact_to_json_sql_func( |
| 3132 | sqlite3_context *context, |
| 3133 | int argc, |
| 3134 | sqlite3_value **argv |
| 3135 | ){ |
| 3136 | int rid = 0; |
| 3137 | Blob b = empty_blob; |
| 3138 | |
| 3139 | if(1 != argc){ |
| 3140 | goto error_usage; |
| 3141 | } |
| 3142 | switch( sqlite3_value_type(argv[0]) ){ |
| 3143 | case SQLITE_INTEGER: |
| 3144 | rid = artifact_to_json_by_rid(sqlite3_value_int(argv[0]), &b); |
| 3145 | break; |
| 3146 | case SQLITE_TEXT:{ |
| 3147 | const char * z = (const char *)sqlite3_value_text(argv[0]); |
| 3148 | if( z ){ |
| 3149 | rid = artifact_to_json_by_name(z, &b); |
| 3150 | } |
| 3151 | break; |
| 3152 | } |
| 3153 | default: |
| 3154 | goto error_usage; |
| 3155 | } |
| 3156 | if( rid>0 ){ |
| 3157 | sqlite3_result_text(context, blob_str(&b), blob_size(&b), |
| 3158 | SQLITE_TRANSIENT); |
| 3159 | blob_reset(&b); |
| 3160 | }else{ |
| 3161 | /* We should arguably error out if rid<0 (ambiguous name) */ |
| 3162 | sqlite3_result_null(context); |
| 3163 | } |
| 3164 | return; |
| 3165 | error_usage: |
| 3166 | sqlite3_result_error(context, "Expecting one argument: blob.rid or " |
| 3167 | "artifact symbolic name", -1); |
| 3168 | } |
| 3169 | |
| 3170 | |
| 3171 | |
| 3172 | /* |
| 3173 | ** COMMAND: test-artifact-to-json |
| 3174 | ** |
| 3175 | ** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names] |
| 3176 | ** |
| 3177 | ** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. |
| 3178 | */ |
| 3179 | void test_manifest_to_json(void){ |
| 3180 | int i; |
| 3181 | Blob b = empty_blob; |
| 3182 | Stmt q; |
| 3183 | const int bPretty = find_option("pretty",0,0)!=0; |
| 3184 | int nErr = 0; |
| 3185 | |
| 3186 | db_find_and_open_repository(0,0); |
| 3187 | db_prepare(&q, "select json_pretty(:json)"); |
| 3188 | for( i=2; i<g.argc; ++i ){ |
| 3189 | char const *zName = g.argv[i]; |
| 3190 | const int rc = artifact_to_json_by_name(zName, &b); |
| 3191 | if( rc<=0 ){ |
| 3192 | ++nErr; |
| 3193 | fossil_warning("Error reading artifact %Q", zName); |
| 3194 | continue; |
| 3195 | }else if( bPretty ){ |
| 3196 | db_bind_blob(&q, ":json", &b); |
| 3197 | b.nUsed = 0; |
| 3198 | db_step(&q); |
| 3199 | db_column_blob(&q, 0, &b); |
| 3200 | db_reset(&q); |
| 3201 | } |
| 3202 | fossil_print("%b\n", &b); |
| 3203 | blob_reset(&b); |
| 3204 | } |
| 3205 | db_finalize(&q); |
| 3206 | if( nErr ){ |
| 3207 | fossil_warning("Error count: %d", nErr); |
| 3208 | } |
| 3209 | } |
| 3210 |