| | @@ -2911,5 +2911,293 @@ |
| 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. The names returned by |
| 2920 | +** this function are geared towards use with artifact_to_json(), and |
| 2921 | +** may differ from some historical uses. e.g. CFTYPE_CONTROL artifacts |
| 2922 | +** are called "tag" artifacts by this function. |
| 2923 | +*/ |
| 2924 | +const char * artifact_type_to_name(int typeId){ |
| 2925 | + switch(typeId){ |
| 2926 | + case CFTYPE_MANIFEST: return "checkin"; |
| 2927 | + case CFTYPE_CLUSTER: return "cluster"; |
| 2928 | + case CFTYPE_CONTROL: return "tag"; |
| 2929 | + case CFTYPE_WIKI: return "wiki"; |
| 2930 | + case CFTYPE_TICKET: return "ticket"; |
| 2931 | + case CFTYPE_ATTACHMENT: return "attachment"; |
| 2932 | + case CFTYPE_EVENT: return "event"; |
| 2933 | + case CFTYPE_FORUM: return "forumpost"; |
| 2934 | + } |
| 2935 | + return NULL; |
| 2936 | +} |
| 2937 | + |
| 2938 | +/* |
| 2939 | +** Creates a JSON representation of p, appending it to b. |
| 2940 | +** |
| 2941 | +** b is not cleared before rendering, so the caller needs to do that |
| 2942 | +** if it's important for their use case. |
| 2943 | +** |
| 2944 | +** Pedantic note: this routine traverses p->aFile directly, rather |
| 2945 | +** than using manifest_file_next(), so that delta manifests are |
| 2946 | +** rendered as-is instead of containing their derived F-cards. If that |
| 2947 | +** policy is ever changed, p will need to be non-const. |
| 2948 | +*/ |
| 2949 | +void artifact_to_json(Manifest const *p, Blob *b){ |
| 2950 | + int i; |
| 2951 | + |
| 2952 | + blob_append_literal(b, "{"); |
| 2953 | + blob_appendf(b, "\"uuid\": \"%z\"", rid_to_uuid(p->rid)); |
| 2954 | + /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ |
| 2955 | + blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type)); |
| 2956 | +#define ISA(TYPE) if( p->type==TYPE ) |
| 2957 | +#define CARD_LETTER(LETTER) \ |
| 2958 | + blob_append_literal(b, ",\"" #LETTER "\": ") |
| 2959 | +#define CARD_STR(LETTER, VAL) \ |
| 2960 | + assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) |
| 2961 | +#define CARD_STR2(LETTER, VAL) \ |
| 2962 | + if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 |
| 2963 | +#define STR_OR_NULL(VAL) \ |
| 2964 | + if( VAL ) blob_appendf(b, "%!j", VAL); \ |
| 2965 | + else blob_append(b, "null", 4) |
| 2966 | +#define KVP_STR(ADDCOMMA, KEY,VAL) \ |
| 2967 | + if(ADDCOMMA) blob_append_char(b, ','); \ |
| 2968 | + blob_appendf(b, "%!j: ", #KEY); \ |
| 2969 | + STR_OR_NULL(VAL) |
| 2970 | + |
| 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 ){ |
| 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 an empty |
| 3041 | + ** array, as per F-cards. */ |
| 3042 | + blob_append_char(b, ']'); |
| 3043 | + } |
| 3044 | + if( p->nCherrypick ){ |
| 3045 | + CARD_LETTER(Q); |
| 3046 | + blob_append_char(b, '['); |
| 3047 | + for( i = 0; i < p->nCherrypick; ++i ){ |
| 3048 | + if( i>0 ) blob_append_char(b, ','); |
| 3049 | + blob_append_char(b, '{'); |
| 3050 | + blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]); |
| 3051 | + KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); |
| 3052 | + KVP_STR(1, base, p->aCherrypick[i].zCPBase); |
| 3053 | + blob_append_char(b, '}'); |
| 3054 | + } |
| 3055 | + blob_append_char(b, ']'); |
| 3056 | + } |
| 3057 | + CARD_STR2(R, p->zRepoCksum); |
| 3058 | + if( p->nTag ){ |
| 3059 | + CARD_LETTER(T); |
| 3060 | + blob_append_char(b, '['); |
| 3061 | + for( int i = 0; i < p->nTag; ++i ){ |
| 3062 | + const char *zName = p->aTag[i].zName; |
| 3063 | + if( i>0 ) blob_append_char(b, ','); |
| 3064 | + blob_append_char(b, '{'); |
| 3065 | + blob_appendf(b, "\"type\": \"%c\"", *zName); |
| 3066 | + KVP_STR(1, name, &zName[1]); |
| 3067 | + KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") |
| 3068 | + /* We could arguably resolve the "*" as null or p's uuid. */; |
| 3069 | + KVP_STR(1, value, p->aTag[i].zValue); |
| 3070 | + blob_append_char(b, '}'); |
| 3071 | + } |
| 3072 | + blob_append_char(b, ']'); |
| 3073 | + } |
| 3074 | + CARD_STR2(U, p->zUser); |
| 3075 | + CARD_STR2(W, p->zWiki); |
| 3076 | + blob_append_literal(b, "}"); |
| 3077 | +#undef CARD_FMT |
| 3078 | +#undef CARD_LETTER |
| 3079 | +#undef CARD_STR |
| 3080 | +#undef CARD_STR2 |
| 3081 | +#undef ISA |
| 3082 | +#undef KVP_STR |
| 3083 | +#undef STR_OR_NULL |
| 3084 | +} |
| 3085 | + |
| 3086 | +/* |
| 3087 | +** Convenience wrapper around artifact_to_json() which expects rid to |
| 3088 | +** be the blob.rid of any artifact type. If it can load a Manifest |
| 3089 | +** with that rid, it returns rid, else it returns 0. |
| 3090 | +*/ |
| 3091 | +int artifact_to_json_by_rid(int rid, Blob *pOut){ |
| 3092 | + Manifest * const p = manifest_get(rid, CFTYPE_ANY, 0); |
| 3093 | + if( p ){ |
| 3094 | + artifact_to_json(p, pOut); |
| 3095 | + manifest_destroy(p); |
| 3096 | + }else{ |
| 3097 | + rid = 0; |
| 3098 | + } |
| 3099 | + return rid; |
| 3100 | +} |
| 3101 | + |
| 3102 | +/* |
| 3103 | +** Convenience wrapper around artifact_to_json() which accepts any |
| 3104 | +** artifact name which is legal for symbolic_name_to_rid(). On success |
| 3105 | +** it returns the rid of the artifact. Returns 0 if no such artifact |
| 3106 | +** exists and a negative value if the name is ambiguous. |
| 3107 | +** |
| 3108 | +** pOut is not cleared before rendering, so the caller needs to do |
| 3109 | +** that if it's important for their use case. |
| 3110 | +*/ |
| 3111 | +int artifact_to_json_by_name(const char *zName, Blob *pOut){ |
| 3112 | + const int rid = symbolic_name_to_rid(zName, 0); |
| 3113 | + return rid>0 |
| 3114 | + ? artifact_to_json_by_rid(rid, pOut) |
| 3115 | + : rid; |
| 3116 | +} |
| 3117 | + |
| 3118 | +/* |
| 3119 | +** SQLite UDF for artifact_to_json(). Its single argument should be |
| 3120 | +** either an INTEGER (blob.rid value) or a TEXT symbolic artifact |
| 3121 | +** name, as per symbolic_name_to_rid(). If an artifact is found then |
| 3122 | +** the result of the UDF is that JSON as a string, else it evaluates |
| 3123 | +** to NULL. |
| 3124 | +*/ |
| 3125 | +void artifact_to_json_sql_func( |
| 3126 | + sqlite3_context *context, |
| 3127 | + int argc, |
| 3128 | + sqlite3_value **argv |
| 3129 | +){ |
| 3130 | + int rid = 0; |
| 3131 | + Blob b = empty_blob; |
| 3132 | + |
| 3133 | + if(1 != argc){ |
| 3134 | + goto error_usage; |
| 3135 | + } |
| 3136 | + switch( sqlite3_value_type(argv[0]) ){ |
| 3137 | + case SQLITE_INTEGER: |
| 3138 | + rid = artifact_to_json_by_rid(sqlite3_value_int(argv[0]), &b); |
| 3139 | + break; |
| 3140 | + case SQLITE_TEXT:{ |
| 3141 | + const char * z = (const char *)sqlite3_value_text(argv[0]); |
| 3142 | + if( z ){ |
| 3143 | + rid = artifact_to_json_by_name(z, &b); |
| 3144 | + } |
| 3145 | + break; |
| 3146 | + } |
| 3147 | + default: |
| 3148 | + goto error_usage; |
| 3149 | + } |
| 3150 | + if( rid>0 ){ |
| 3151 | + sqlite3_result_text(context, blob_str(&b), blob_size(&b), |
| 3152 | + SQLITE_TRANSIENT); |
| 3153 | + blob_reset(&b); |
| 3154 | + }else{ |
| 3155 | + /* We should arguably error out if rid<0 (ambiguous name) */ |
| 3156 | + sqlite3_result_null(context); |
| 3157 | + } |
| 3158 | + return; |
| 3159 | +error_usage: |
| 3160 | + sqlite3_result_error(context, "Expecting one argument: blob.rid or " |
| 3161 | + "artifact symbolic name", -1); |
| 3162 | +} |
| 3163 | + |
| 3164 | + |
| 3165 | + |
| 3166 | +/* |
| 3167 | +** COMMAND: test-artifact-to-json |
| 3168 | +** |
| 3169 | +** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names] |
| 3170 | +** |
| 3171 | +** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. |
| 3172 | +*/ |
| 3173 | +void test_manifest_to_json(void){ |
| 3174 | + int i; |
| 3175 | + Blob b = empty_blob; |
| 3176 | + Stmt q; |
| 3177 | + const int bPretty = find_option("pretty",0,0)!=0; |
| 3178 | + int nErr = 0; |
| 3179 | + |
| 3180 | + db_find_and_open_repository(0,0); |
| 3181 | + db_prepare(&q, "select json_pretty(:json)"); |
| 3182 | + for( i=2; i<g.argc; ++i ){ |
| 3183 | + char const *zName = g.argv[i]; |
| 3184 | + const int rc = artifact_to_json_by_name(zName, &b); |
| 3185 | + if( rc<=0 ){ |
| 3186 | + ++nErr; |
| 3187 | + fossil_warning("Error reading artifact %Q", zName); |
| 3188 | + continue; |
| 3189 | + }else if( bPretty ){ |
| 3190 | + db_bind_blob(&q, ":json", &b); |
| 3191 | + b.nUsed = 0; |
| 3192 | + db_step(&q); |
| 3193 | + db_column_blob(&q, 0, &b); |
| 3194 | + db_reset(&q); |
| 3195 | + } |
| 3196 | + fossil_print("%b\n", &b); |
| 3197 | + blob_reset(&b); |
| 3198 | + } |
| 3199 | + db_finalize(&q); |
| 3200 | + if( nErr ){ |
| 3201 | + fossil_warning("Error count: %d", nErr); |
| 3202 | + } |
| 3203 | +} |
| 2916 | 3204 | |