Browse files

Now passes CouchDB "basics.js" tests

Lots and lots of little fixes and compatibility tweaks.
I did have to comment out a few lines in the tests that required exact error message strings.
  • Loading branch information...
1 parent aac1fa2 commit e23509cc8f0f230b96ebc4162ec84e7229a40e1b @snej snej committed Jan 3, 2012
View
32 Demo-Mac/DemoAppController.m
@@ -34,6 +34,10 @@ int main (int argc, const char * argv[]) {
static TDListener* sListener;
+@interface DemoAppController () <TDViewCompiler>
+@end
+
+
@implementation DemoAppController
@@ -101,6 +105,7 @@ - (void) applicationDidFinishLaunching: (NSNotification*)n {
object: nil];
// Start a listener socket:
+ [TDView setCompiler: self];
sListener = [[TDListener alloc] initWithTDServer: server.touchServer port: 8888];
[sListener start];
}
@@ -209,6 +214,33 @@ - (void) replicationProgressChanged: (NSNotification*)n {
}
+- (TDMapBlock) compileMapFunction: (NSString*)mapSource language:(NSString *)language {
+ if (!$equal(language, @"javascript"))
+ return NULL;
+ TDMapBlock mapBlock = NULL;
+ if ($equal(mapSource, @"(function (doc) {if (doc.a == 4) {emit(null, doc.b);}})")) {
+ mapBlock = ^(NSDictionary* doc, TDMapEmitBlock emit) {
+ if ($equal([doc objectForKey: @"a"], $object(4)))
+ emit(nil, [doc objectForKey: @"b"]);
+ };
+ }
+ return [[mapBlock copy] autorelease];
+}
+
+
+- (TDReduceBlock) compileReduceFunction: (NSString*)reduceSource language:(NSString *)language {
+ if (!$equal(language, @"javascript"))
+ return NULL;
+ TDReduceBlock reduceBlock = NULL;
+ if ($equal(reduceSource, @"(function (keys, values) {return sum(values);})")) {
+ reduceBlock = ^(NSArray* keys, NSArray* values, BOOL rereduce) {
+ return [TDView totalValues: values];
+ };
+ }
+ return [[reduceBlock copy] autorelease];
+}
+
+
#pragma mark HIGHLIGHTING NEW ITEMS:
View
1 Listener/TDHTTPConnection.h
@@ -7,7 +7,6 @@
//
#import "HTTPConnection.h"
-@class TDRouter;
@interface TDHTTPConnection : HTTPConnection
View
47 Listener/TDHTTPConnection.m
@@ -6,36 +6,43 @@
// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
//
+// Based on CocoaHTTPServer/Samples/PostHTTPServer/MyHTTPConnection.m
+
#import "TDHTTPConnection.h"
#import "TDHTTPServer.h"
#import "TDHTTPResponse.h"
#import "TDListener.h"
#import "TDServer.h"
#import "TDRouter.h"
+
#import "HTTPMessage.h"
#import "HTTPDataResponse.h"
+#import "Test.h"
+
@implementation TDHTTPConnection
-- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
-{
- return YES;
+- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path {
+ return $equal(method, @"POST") || $equal(method, @"PUT") || $equal(method, @"DELETE")
+ || [super supportsMethod: method atPath: path];
}
- (NSObject<HTTPResponse>*)httpResponseForMethod:(NSString *)method URI:(NSString *)path {
- NSLog(@"TDListener: %@ %@", method, path); //TEMP
- NSURL* url = [NSURL URLWithString: [@"touchdb://" stringByAppendingString: path]];
- NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL: url];
+ LogTo(TDListener, @"%@ %@ {+%u}", method, path, (unsigned)request.body.length);
+ AssertEq(request.body.length, requestContentLength);
+
+ // Construct an NSURLRequest from the HTTPRequest:
+ NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL: request.url];
urlRequest.HTTPMethod = method;
urlRequest.HTTPBody = request.body;
NSDictionary* headers = request.allHeaderFields;
- for (NSString* header in headers) {
+ for (NSString* header in headers)
[urlRequest setValue: [headers objectForKey: header] forHTTPHeaderField: header];
- }
+ // Create a TDRouter:
TDRouter* router = [[TDRouter alloc] initWithServer: ((TDHTTPServer*)config.server).tdServer
request: urlRequest];
__block bool finished = false;
@@ -51,9 +58,13 @@ - (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
finished = true;
};
+ // Run the router, synchronously:
[((TDHTTPServer*)config.server).listener onServerThread: ^{[router start];}];
NSAssert(finished, @"Router didn't finish");
+ LogTo(TDListener, @"%@ %@ --> %i", method, path, routerResponse.status);
+
+ // Return the response:
#if DEBUG
BOOL pretty = YES;
#else
@@ -67,4 +78,24 @@ - (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
}
+- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path {
+ return $equal(method, @"POST") || $equal(method, @"PUT")
+ || [super expectsRequestBodyFromMethod:method atPath:path];
+}
+
+- (void)prepareForBodyWithSize:(UInt64)contentLength {
+ // Could use this method to open a temp file for large uploads
+}
+
+- (void)processBodyData:(NSData *)postDataChunk {
+ // Remember: In order to support LARGE POST uploads, the data is read in chunks.
+ // This prevents a 50 MB upload from being stored in RAM.
+ // The size of the chunks are limited by the POST_CHUNKSIZE definition.
+ // Therefore, this method may be called multiple times for the same POST request.
+
+ if (![request appendData:postDataChunk])
+ Warn(@"TDHTTPConnection: couldn't append data chunk");
+}
+
+
@end
View
2 Listener/TDHTTPResponse.m
@@ -19,7 +19,7 @@ - (id) initWithTDResponse: (TDResponse*)response pretty: (BOOL)pretty {
int status = response.status;
if (!responseBody && status >= 300) {
// Put a generic error message in the body:
- responseBody = [[NSString stringWithFormat: @"%d %@\n",
+ responseBody = [[NSString stringWithFormat: @"{\"status\": %i, \"error\":\"%@\"}\n",
status, [NSHTTPURLResponse localizedStringForStatusCode: status]]
dataUsingEncoding: NSUTF8StringEncoding];
[response.headers setObject: @"text/plain; encoding=UTF-8" forKey: @"Content-Type"];
View
5 Source/TDBody.m
@@ -97,9 +97,10 @@ - (NSString*) asJSONString {
- (id) asObject {
if (!_object && !_error) {
- _object = [[NSJSONSerialization JSONObjectWithData: _json options: 0 error: nil] copy];
+ NSError* error = nil;
+ _object = [[NSJSONSerialization JSONObjectWithData: _json options: 0 error: &error] copy];
if (!_object) {
- Warn(@"TDBody: couldn't parse JSON");
+ Warn(@"TDBody: couldn't parse JSON: %@ (error=%@)", [_json my_UTF8ToString], error);
_error = YES;
}
}
View
44 Source/TDDatabase+Insertion.m
@@ -62,7 +62,7 @@ + (BOOL) isValidDocumentID: (NSString*)str {
return [str autorelease];
}
-- (NSString*) generateDocumentID {
++ (NSString*) generateDocumentID {
return createUUID();
}
@@ -105,13 +105,30 @@ - (SInt64) getOrInsertDocNumericID: (NSString*)docID {
- (NSData*) encodeDocumentJSON: (TDRevision*)rev {
- NSMutableDictionary* properties = [rev.properties mutableCopy];
- if (!properties)
+ static NSSet* sKnownSpecialKeys;
+ if (!sKnownSpecialKeys) {
+ sKnownSpecialKeys = [[NSSet alloc] initWithObjects: @"_id", @"_rev",
+ @"_attachments", @"_deleted", nil];
+ }
+
+ NSDictionary* origProps = rev.properties;
+ if (!origProps)
return nil;
- [properties removeObjectForKey: @"_id"];
- [properties removeObjectForKey: @"_rev"];
- [properties removeObjectForKey: @"_attachments"];
- [properties removeObjectForKey: @"_deleted"];
+
+ // Don't allow any "_"-prefixed keys. Known ones we'll ignore, unknown ones are an error.
+ NSMutableDictionary* properties = [[NSMutableDictionary alloc] initWithCapacity: origProps.count];
+ for (NSString* key in origProps) {
+ if ([key hasPrefix: @"_"]) {
+ if (![sKnownSpecialKeys member: key]) {
+ Log(@"TDDatabase: Invalid top-level key '%@' in document to be inserted", key);
+ [properties release];
+ return nil;
+ }
+ } else {
+ [properties setObject: [origProps objectForKey: key] forKey: key];
+ }
+ }
+
NSError* error;
NSData* json = [NSJSONSerialization dataWithJSONObject: properties options:0 error: &error];
[properties release];
@@ -149,7 +166,7 @@ - (TDRevision*) putRevision: (TDRevision*)rev
NSString* docID = rev.docID;
SInt64 docNumericID;
BOOL deleted = rev.deleted;
- if (!rev || (prevRevID && !docID) || (deleted && !prevRevID)) {
+ if (!rev || (prevRevID && !docID) || (deleted && !docID)) {
*outStatus = 400;
return nil;
}
@@ -169,9 +186,7 @@ - (TDRevision*) putRevision: (TDRevision*)rev
$object(docNumericID), prevRevID];
if (parentSequence == 0) {
// Not found: 404 or a 409, depending on whether there is any current revision
- TDRevision* cur = [self getDocumentWithID: docID revisionID: nil
- withAttachments: NO];
- *outStatus = cur ? 409 : 404;
+ *outStatus = [self existsDocumentWithID: docID revisionID: nil] ? 409 : 404;
return nil;
}
@@ -192,6 +207,11 @@ - (TDRevision*) putRevision: (TDRevision*)rev
$object(parentSequence)])
return nil;
} else if (docID) {
+ if (deleted) {
+ // Didn't specify a revision to delete: 404 or a 409, depending
+ *outStatus = [self existsDocumentWithID: docID revisionID: nil] ? 409 : 404;
+ return nil;
+ }
// Inserting first revision, with docID given: make sure docID doesn't exist,
// or exists but is currently deleted
if (![self validateRevision: rev previousRevision: nil]) {
@@ -221,7 +241,7 @@ - (TDRevision*) putRevision: (TDRevision*)rev
r = nil;
} else {
// Inserting first revision, with no docID given: generate a unique docID:
- docID = [self generateDocumentID];
+ docID = [[self class] generateDocumentID];
docNumericID = [self insertDocumentID: docID];
if (docNumericID <= 0)
return nil;
View
7 Source/TDDatabase.h
@@ -35,6 +35,7 @@ typedef BOOL (^TDFilterBlock) (TDRevision* revision);
{
@private
NSString* _path;
+ NSString* _name;
FMDatabase *_fmdb;
BOOL _open;
NSInteger _transactionLevel;
@@ -53,7 +54,7 @@ typedef BOOL (^TDFilterBlock) (TDRevision* revision);
+ (TDDatabase*) createEmptyDBAtPath: (NSString*)path;
@property (readonly) NSString* path;
-@property (readonly) NSString* name;
+@property (readonly, copy) NSString* name;
@property (readonly) BOOL exists;
/** Begins a database transaction. Transactions can nest. Every -beginTransaction must be balanced by a later -endTransaction:. */
@@ -74,6 +75,8 @@ typedef BOOL (^TDFilterBlock) (TDRevision* revision);
- (TDRevision*) getDocumentWithID: (NSString*)docID
revisionID: (NSString*)revID
withAttachments: (BOOL)withAttachments;
+- (BOOL) existsDocumentWithID: (NSString*)docID
+ revisionID: (NSString*)revID;
- (TDStatus) loadRevisionBody: (TDRevision*)rev
withAttachments: (BOOL)andAttachments;
@@ -108,7 +111,7 @@ typedef BOOL (^TDFilterBlock) (TDRevision* revision);
@interface TDDatabase (Insertion)
+ (BOOL) isValidDocumentID: (NSString*)str;
-- (NSString*) generateDocumentID;
++ (NSString*) generateDocumentID;
/** Stores a new (or initial) revision of a document. This is what's invoked by a PUT or POST. As with those, the previous revision ID must be supplied when necessary and the call will fail if it doesn't match.
@param revision The revision to add. If the docID is nil, a new UUID will be assigned. Its revID must be nil. It must have a JSON body.
View
20 Source/TDDatabase.m
@@ -45,7 +45,9 @@ + (TDDatabase*) createEmptyDBAtPath: (NSString*)path {
- (id) initWithPath: (NSString*)path {
if (self = [super init]) {
+ Assert([path hasPrefix: @"/"], @"Path must be absolute");
_path = [path copy];
+ _name = [path.lastPathComponent.stringByDeletingPathExtension copy];
_fmdb = [[FMDatabase alloc] initWithPath: _path];
_fmdb.busyRetryTimeout = 10;
#if DEBUG
@@ -59,7 +61,7 @@ - (id) initWithPath: (NSString*)path {
}
- (NSString*) description {
- return $sprintf(@"%@[%@]", [self class], _fmdb.databasePath);
+ return $sprintf(@"%@[%@]", [self class], _path);
}
- (BOOL) exists {
@@ -206,15 +208,7 @@ - (void) dealloc {
[super dealloc];
}
-@synthesize fmdb=_fmdb, attachmentStore=_attachments;
-
-- (NSString*) path {
- return _fmdb.databasePath;
-}
-
-- (NSString*) name {
- return _fmdb.databasePath.lastPathComponent.stringByDeletingPathExtension;
-}
+@synthesize path=_path, name=_name, fmdb=_fmdb, attachmentStore=_attachments;
- (BOOL) beginTransaction {
@@ -373,6 +367,12 @@ - (TDRevision*) getDocumentWithID: (NSString*)docID
}
+- (BOOL) existsDocumentWithID: (NSString*)docID revisionID: (NSString*)revID {
+ return [self getDocumentWithID: docID revisionID: revID withAttachments: NO] != nil;
+ //OPT: Do this without loading the data
+}
+
+
- (TDStatus) loadRevisionBody: (TDRevision*)rev
withAttachments: (BOOL)withAttachments
{
View
9 Source/TDDatabase_Tests.m
@@ -101,14 +101,21 @@
return [[revision.properties objectForKey: @"status"] isEqual: @"not updated!"];
}];
CAssertEq(changes.count, 0u);
-
+
// Delete it:
TDRevision* revD = [[[TDRevision alloc] initWithDocID: rev2.docID revID: nil deleted: YES] autorelease];
+ CAssertEq([db putRevision: revD prevRevisionID: nil status: &status], nil);
+ CAssertEq(status, 409);
revD = [db putRevision: revD prevRevisionID: rev2.revID status: &status];
CAssertEq(status, 200);
CAssertEqual(revD.docID, rev2.docID);
CAssert([revD.revID hasPrefix: @"3-"]);
+ // Delete nonexistent doc:
+ TDRevision* revFake = [[[TDRevision alloc] initWithDocID: @"fake" revID: nil deleted: YES] autorelease];
+ [db putRevision: revFake prevRevisionID: nil status: &status];
+ CAssertEq(status, 404);
+
// Read it back (should fail):
readRev = [db getDocumentWithID: revD.docID revisionID: nil withAttachments: NO];
CAssertNil(readRev);
View
1 Source/TDInternal.h
@@ -14,6 +14,7 @@
@interface TDDatabase ()
+@property (readwrite, copy) NSString* name; // make it settable
@property (readonly) FMDatabase* fmdb;
@property (readonly) TDBlobStore* attachmentStore;
- (SInt64) getDocNumericID: (NSString*)docID;
View
203 Source/TDRouter.m
@@ -93,6 +93,20 @@ - (BOOL) boolQuery: (NSString*)param {
return value && !$equal(value, @"false") && !$equal(value, @"0");
}
+- (int) intQuery: (NSString*)param defaultValue: (int)defaultValue {
+ NSString* value = [self.queries objectForKey: param];
+ return value ? value.intValue : defaultValue;
+}
+
+- (id) jsonQuery: (NSString*)param error: (NSError**)outError {
+ *outError = nil;
+ NSString* value = [self query: @"startkey"];
+ if (!value)
+ return nil;
+ return [NSJSONSerialization JSONObjectWithData: [value dataUsingEncoding: NSUTF8StringEncoding]
+ options: NSJSONReadingAllowFragments error: outError];
+}
+
- (TDStatus) openDB {
if (!_db.exists)
@@ -103,9 +117,20 @@ - (TDStatus) openDB {
}
-static NSArray* splitPath( NSString* path ) {
- return [[path componentsSeparatedByString: @"/"]
- my_filter: ^(id component) {return [component length] > 0;}];
+static NSArray* splitPath( NSURL* url ) {
+ // Unfortunately can't just call url.path because that converts %2F to a '/'.
+ NSString* pathString = NSMakeCollectable(CFURLCopyPath((CFURLRef)url));
+ NSMutableArray* path = $marray();
+ for (NSString* comp in [pathString componentsSeparatedByString: @"/"]) {
+ if ([comp length] > 0) {
+ comp = [comp stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ if (!comp)
+ return nil; // bad URL
+ [path addObject: comp];
+ }
+ }
+ [pathString release];
+ return path;
}
@@ -129,7 +154,12 @@ - (void) start {
NSMutableString* message = [NSMutableString stringWithFormat: @"do_%@", method];
// First interpret the components of the request:
- _path = [splitPath(_request.URL.path) mutableCopy];
+ _path = [splitPath(_request.URL) mutableCopy];
+ if (!_path) {
+ _response.status = 400;
+ return;
+ }
+
NSUInteger pathLen = _path.count;
if (pathLen > 0) {
NSString* dbName = [_path objectAtIndex: 0];
@@ -211,6 +241,9 @@ - (void) start {
TDStatus status = (TDStatus) objc_msgSend(self, sel, _db, docID, attachmentName);
// Configure response headers:
+ if (status < 300 && !_response.body && ![_response.headers objectForKey: @"Content-Type"]) {
+ _response.body = [TDBody bodyWithJSON: [@"{\"ok\":true}" dataUsingEncoding: NSUTF8StringEncoding]];
+ }
if (_response.body.isValidJSON)
[_response setValue: @"application/json" ofHeader: @"Content-Type"];
@@ -247,7 +280,9 @@ - (TDStatus) do_UNKNOWN {
- (TDStatus) do_GETRoot {
- NSDictionary* info = $dict({@"TouchDB", @"welcome"}, {@"version", kTDVersionString});
+ NSDictionary* info = $dict({@"TouchDB", @"Welcome"},
+ {@"couchdb", @"Welcome"}, // for compatibility
+ {@"version", kTDVersionString});
_response.body = [TDBody bodyWithProperties: info];
return 200;
}
@@ -312,6 +347,16 @@ - (TDStatus) do_POST_replicate {
}
+- (TDStatus) do_GET_uuids {
+ int count = MIN(1000, [self intQuery: @"count" defaultValue: 1]);
+ NSMutableArray* uuids = [NSMutableArray arrayWithCapacity: count];
+ for (int i=0; i<count; i++)
+ [uuids addObject: [TDDatabase generateDocumentID]];
+ _response.bodyObject = $dict({@"uuids", uuids});
+ return 200;
+}
+
+
- (TDStatus) do_GET_active_tasks {
// http://wiki.apache.org/couchdb/HttpGetActiveTasks
NSMutableArray* activity = $marray();
@@ -355,7 +400,7 @@ - (TDStatus) do_GET: (TDDatabase*)db {
if (num_docs == NSNotFound || update_seq == NSNotFound)
return 500;
_response.bodyObject = $dict({@"db_name", db.name},
- {@"num_docs", $object(num_docs)},
+ {@"doc_count", $object(num_docs)},
{@"update_seq", $object(update_seq)});
return 200;
}
@@ -364,11 +409,16 @@ - (TDStatus) do_GET: (TDDatabase*)db {
- (TDStatus) do_PUT: (TDDatabase*)db {
if (db.exists)
return 412;
- return [db open] ? 201 : 500;
+ if (![db open])
+ return 500;
+ [_response setValue: _request.URL.absoluteString ofHeader: @"Location"];
+ return 201;
}
- (TDStatus) do_DELETE: (TDDatabase*)db {
+ if ([self query: @"rev"])
+ return 400; // CouchDB checks for this; probably meant to be a document deletion
return [_server deleteDatabaseNamed: db.name] ? 200 : 404;
}
@@ -377,29 +427,30 @@ - (TDStatus) do_POST: (TDDatabase*)db {
TDStatus status = [self openDB];
if (status >= 300)
return status;
- NSString* docID = [db generateDocumentID];
- status = [self update: db docID: docID json: _request.HTTPBody deleting: NO];
- if (status == 201) {
- NSURL* url = [_request.URL URLByAppendingPathComponent: docID];
- [_response.headers setObject: url.absoluteString forKey: @"Location"];
- }
- return status;
+ return [self update: db docID: nil json: _request.HTTPBody deleting: NO];
}
- (BOOL) getQueryOptions: (TDQueryOptions*)options {
// http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
*options = kDefaultTDQueryOptions;
- NSString* param = [self query: @"limit"];
- if (param)
- options->limit = param.intValue;
- param = [self query: @"skip"];
- if (param)
- options->skip = param.intValue;
+ options->skip = [self intQuery: @"skip" defaultValue: options->skip];
+ options->limit = [self intQuery: @"limit" defaultValue: options->limit];
+ options->groupLevel = [self intQuery: @"group_level" defaultValue: options->groupLevel];
options->descending = [self boolQuery: @"descending"];
options->includeDocs = [self boolQuery: @"include_docs"];
options->updateSeq = [self boolQuery: @"update_seq"];
- return YES;
+ if ([self query: @"inclusive_end"])
+ options->inclusiveEnd = [self boolQuery: @"inclusive_end"];
+ options->reduce = [self boolQuery: @"reduce"];
+ options->group = [self boolQuery: @"group"];
+ NSError* error = nil;
+ options->startKey = [self jsonQuery: @"startkey" error: &error];
+ if (error)
+ return NO;
+ if (!error)
+ options->endKey = [self jsonQuery: @"endkey" error: &error];
+ return !error;
}
@@ -419,6 +470,10 @@ - (TDStatus) do_POST_compact: (TDDatabase*)db {
return [db compact];
}
+- (TDStatus) do_POST_ensure_full_commit: (TDDatabase*)db {
+ return 200;
+}
+
#pragma mark - CHANGES:
@@ -525,6 +580,7 @@ - (NSString*) setResponseEtag: (TDRevision*)rev {
- (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID {
+ // http://wiki.apache.org/couchdb/HTTP_Document_API#GET
TDRevision* rev = [db getDocumentWithID: docID
revisionID: [self query: @"rev"] // often nil
withAttachments: [self boolQuery: @"attachments"]];
@@ -536,8 +592,34 @@ - (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID {
if ($equal(eTag, [_request valueForHTTPHeaderField: @"If-None-Match"]))
return 304;
- _response.body = rev.body;
- //TODO: Handle ?_revs_info query
+ NSMutableDictionary* extra = $mdict();
+
+ if ([self boolQuery: @"local_seq"])
+ [extra setObject: $object(rev.sequence) forKey: @"_local_seq"];
+
+ if ([self boolQuery: @"revs_info"]) {
+ NSArray* info = [_db getRevisionHistory: rev];
+ if (!info)
+ return 500;
+ info = [info my_map: ^id(id rev) {
+ NSString* status = @"available";
+ if ([rev deleted])
+ status = @"deleted";
+ // TODO: Detect missing revisions, set status="missing"
+ return $dict({@"rev", [rev revID]}, {@"status", status});
+ }];
+
+ [extra setObject: info forKey: @"_revs_info"];
+ }
+
+ TDBody* responseBody = rev.body;
+ if (extra.count) {
+ NSMutableDictionary* props = [responseBody.properties mutableCopy];
+ [props addEntriesFromDictionary: extra];
+ responseBody = [TDBody bodyWithProperties: props];
+ // OPT: More efficient to use appendDictToJSON, like TDDocument
+ }
+ _response.body = responseBody;
return 200;
}
@@ -575,15 +657,23 @@ - (TDStatus) update: (TDDatabase*)db
json: (NSData*)json
deleting: (BOOL)deleting
{
+ BOOL posting = (docID == nil);
TDBody* body = json ? [TDBody bodyWithJSON: json] : nil;
NSString* revID;
if (!deleting) {
+ deleting = $castIf(NSNumber, [body propertyForKey: @"_deleted"]).boolValue;
+ if (!docID) {
+ // POST's doc ID may come from the _id field of the JSON body, else generate a random one.
+ docID = [body propertyForKey: @"_id"];
+ if (!docID) {
+ if (deleting)
+ return 400;
+ docID = [TDDatabase generateDocumentID];
+ }
+ }
// PUT's revision ID comes from the JSON body.
revID = [body propertyForKey: @"_rev"];
- deleting = $castIf(NSNumber, [body propertyForKey: @"_deleted"]).boolValue;
- if (deleting && !docID)
- return 400; // POST and _deleted don't mix
} else {
// DELETE's revision ID can come either from the ?rev= query param or an If-Match header.
revID = [self query: @"rev"];
@@ -609,6 +699,12 @@ - (TDStatus) update: (TDDatabase*)db
rev = [db putRevision: rev prevRevisionID: revID status: &status];
if (status < 300) {
[self setResponseEtag: rev];
+ if (!deleting) {
+ NSURL* url = _request.URL;
+ if (posting)
+ url = [url URLByAppendingPathComponent: docID];
+ [_response.headers setObject: url.absoluteString forKey: @"Location"];
+ }
_response.bodyObject = $dict({@"ok", $true},
{@"id", rev.docID},
{@"rev", rev.revID});
@@ -630,7 +726,7 @@ - (TDStatus) do_DELETE: (TDDatabase*)db docID: (NSString*)docID {
}
-#pragma mark - DESIGN DOCS:
+#pragma mark - VIEW QUERIES:
- (TDStatus) do_GET: (TDDatabase*)db designDocID: (NSString*)designDoc view: (NSString*)viewName {
@@ -656,6 +752,59 @@ - (TDStatus) do_GET: (TDDatabase*)db designDocID: (NSString*)designDoc view: (NS
}
+- (TDStatus) do_POST_temp_view: (TDDatabase*)db {
+ if (![[_request valueForHTTPHeaderField: @"Content-Type"] hasPrefix: @"application/json"])
+ return 415;
+ TDBody* requestBody = [TDBody bodyWithJSON: _request.HTTPBody];
+ NSDictionary* props = requestBody.properties;
+ if (!props)
+ return 400;
+
+ TDQueryOptions options;
+ if (![self getQueryOptions: &options])
+ return 400;
+
+ TDView* view = [_db viewNamed: @"@@TEMP@@"];
+ if (!view)
+ return 500;
+ @try {
+ NSString* language = [props objectForKey: @"language"] ?: @"javascript";
+ NSString* mapSource = [props objectForKey: @"map"];
+ TDMapBlock mapBlock = [[TDView compiler] compileMapFunction: mapSource language: language];
+ if (!mapBlock) {
+ Warn(@"Unknown map function source: %@", mapSource);
+ return 500;
+ }
+ NSString* reduceSource = [props objectForKey: @"reduce"];
+ TDReduceBlock reduceBlock = NULL;
+ if (reduceSource) {
+ reduceBlock =[[TDView compiler] compileReduceFunction: reduceSource language: language];
+ if (!reduceBlock) {
+ Warn(@"Unknown reduce function source: %@", reduceSource);
+ return 500;
+ }
+ }
+
+ [view setMapBlock: mapBlock reduceBlock: reduceBlock version: @"1"];
+ if (reduceBlock)
+ options.reduce = YES;
+
+ TDStatus status;
+ NSArray* rows = [view queryWithOptions: &options status: &status];
+ if (!rows)
+ return status;
+ id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
+ _response.bodyObject = $dict({@"rows", rows},
+ {@"total_rows", $object(rows.count)},
+ {@"offset", $object(options.skip)},
+ {@"update_seq", updateSeq});
+ return 200;
+ } @finally {
+ [view deleteView];
+ }
+}
+
+
@end
View
12 Source/TDRouter_Tests.m
@@ -72,7 +72,8 @@ static id Send(TDServer* server, NSString* method, NSString* path,
TestCase(TDRouter_Server) {
RequireTestCase(TDServer);
TDServer* server = [TDServer createEmptyAtPath: @"/tmp/TDRouterTest"];
- Send(server, @"GET", @"/", 200, $dict({@"TouchDB", @"welcome"},
+ Send(server, @"GET", @"/", 200, $dict({@"TouchDB", @"Welcome"},
+ {@"couchdb", @"Welcome"},
{@"version", kTDVersionString}));
Send(server, @"GET", @"/_all_dbs", 200, $array());
Send(server, @"GET", @"/non-existent", 404, nil);
@@ -87,14 +88,19 @@ static id Send(TDServer* server, NSString* method, NSString* path,
TDServer* server = [TDServer createEmptyAtPath: @"/tmp/TDRouterTest"];
Send(server, @"PUT", @"/database", 201, nil);
Send(server, @"GET", @"/database", 200,
- $dict({@"db_name", @"database"}, {@"num_docs", $object(0)}, {@"update_seq", $object(0)}));
+ $dict({@"db_name", @"database"}, {@"doc_count", $object(0)}, {@"update_seq", $object(0)}));
Send(server, @"PUT", @"/database", 412, nil);
Send(server, @"PUT", @"/database2", 201, nil);
Send(server, @"GET", @"/_all_dbs", 200, $array(@"database", @"database2"));
Send(server, @"GET", @"/database2", 200,
- $dict({@"db_name", @"database2"}, {@"num_docs", $object(0)}, {@"update_seq", $object(0)}));
+ $dict({@"db_name", @"database2"}, {@"doc_count", $object(0)}, {@"update_seq", $object(0)}));
Send(server, @"DELETE", @"/database2", 200, nil);
Send(server, @"GET", @"/_all_dbs", 200, $array(@"database"));
+
+ Send(server, @"PUT", @"/database%2Fwith%2Fslashes", 201, nil);
+ Send(server, @"GET", @"/database%2Fwith%2Fslashes", 200,
+ $dict({@"db_name", @"database/with/slashes"},
+ {@"doc_count", $object(0)}, {@"update_seq", $object(0)}));
}
View
5 Source/TDServer.m
@@ -15,6 +15,7 @@
#import "TDServer.h"
#import "TDDatabase.h"
+#import "TDInternal.h"
@implementation TDServer
@@ -23,7 +24,7 @@ @implementation TDServer
#define kDBExtension @"touchdb"
// http://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
-#define kLegalChars @"abcdefghijklmnopqrstuvwxyz0123456789_$()+-"
+#define kLegalChars @"abcdefghijklmnopqrstuvwxyz0123456789_$()+-/"
static NSCharacterSet* kIllegalNameChars;
+ (void) initialize {
@@ -83,6 +84,7 @@ - (NSString*) pathForName: (NSString*)name {
if (name.length == 0 || [name rangeOfCharacterFromSet: kIllegalNameChars].length > 0
|| !islower([name characterAtIndex: 0]))
return nil;
+ name = [name stringByReplacingOccurrencesOfString: @"/" withString: @":"];
return [_dir stringByAppendingPathComponent:[name stringByAppendingPathExtension:kDBExtension]];
}
@@ -97,6 +99,7 @@ - (TDDatabase*) databaseNamed: (NSString*)name create: (BOOL)create {
[db release];
return nil;
}
+ db.name = name;
[_databases setObject: db forKey: name];
[db release];
}
View
2 Source/TDURLProtocol.m
@@ -141,7 +141,7 @@ - (void)stopLoading {
CAssertNil(error);
CAssertEq(response.statusCode, 200);
CAssertEqual([response.allHeaderFields objectForKey: @"Content-Type"], @"application/json");
- CAssert([bodyStr rangeOfString: @"\"TouchDB\":\"welcome\""].length > 0);
+ CAssert([bodyStr rangeOfString: @"\"TouchDB\":\"Welcome\""].length > 0);
}
#endif
View
13 Source/TDView.h
@@ -43,6 +43,13 @@ typedef struct TDQueryOptions {
extern const TDQueryOptions kDefaultTDQueryOptions;
+/** An external object that knows how to map source code of some sort into executable functions. */
+@protocol TDViewCompiler <NSObject>
+- (TDMapBlock) compileMapFunction: (NSString*)mapSource language: (NSString*)language;
+- (TDReduceBlock) compileReduceFunction: (NSString*)reduceSource language: (NSString*)language;
+@end
+
+
/** Represents a view available in a database. */
@interface TDView : NSObject
{
@@ -77,4 +84,10 @@ extern const TDQueryOptions kDefaultTDQueryOptions;
- (NSArray*) queryWithOptions: (const TDQueryOptions*)options
status: (TDStatus*)outStatus;
+/** Utility function to use in reduce blocks. Totals an array of NSNumbers. */
++ (NSNumber*) totalValues: (NSArray*)values;
+
++ (void) setCompiler: (id<TDViewCompiler>)compiler;
++ (id<TDViewCompiler>) compiler;
+
@end
View
23 Source/TDView.m
@@ -31,6 +31,9 @@
};
+static id<TDViewCompiler> sCompiler;
+
+
@implementation TDView
@@ -185,7 +188,7 @@ - (TDStatus) updateIndex {
// that's called down below.
TDMapEmitBlock emit = ^(id key, id value) {
if (!key)
- return;
+ key = $null;
NSString* keyJSON = toJSONString(key);
NSString* valueJSON = toJSONString(value);
LogTo(View, @" emit(%@, %@)", keyJSON, valueJSON);
@@ -438,4 +441,22 @@ - (NSArray*) dump {
}
++ (NSNumber*) totalValues: (NSArray*)values {
+ double total = 0;
+ for (NSNumber* value in values)
+ total += value.doubleValue;
+ return [NSNumber numberWithDouble: total];
+}
+
+
++ (void) setCompiler: (id<TDViewCompiler>)compiler {
+ [sCompiler autorelease];
+ sCompiler = [compiler retain];
+}
+
++ (id<TDViewCompiler>) compiler {
+ return sCompiler;
+}
+
+
@end
View
15 Source/TDView_Tests.m
@@ -77,20 +77,13 @@
[view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
CAssert([doc objectForKey: @"_id"] != nil, @"Missing _id in %@", doc);
CAssert([doc objectForKey: @"_rev"] != nil, @"Missing _rev in %@", doc);
- emit([doc objectForKey: @"key"], nil);
+ if ([doc objectForKey: @"key"])
+ emit([doc objectForKey: @"key"], nil);
} reduceBlock: NULL version: @"1"];
return view;
}
-static id total(NSArray* keys, NSArray* values) {
- double total = 0;
- for (NSNumber* value in values)
- total += value.doubleValue;
- return $object(total);
-}
-
-
TestCase(TDView_Index) {
RequireTestCase(TDView_Create);
TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
@@ -255,7 +248,7 @@ static id total(NSArray* keys, NSArray* values) {
if (cost)
emit([doc objectForKey: @"_id"], cost);
} reduceBlock: ^(NSArray* keys, NSArray* values, BOOL rereduce) {
- return total(keys, values);
+ return [TDView totalValues: values];
} version: @"1"];
CAssertEq([view updateIndex], 200);
@@ -297,7 +290,7 @@ static id total(NSArray* keys, NSArray* values) {
[doc objectForKey: @"track"]),
[doc objectForKey: @"time"]);
} reduceBlock:^id(NSArray *keys, NSArray *values, BOOL rereduce) {
- return total(keys, values);
+ return [TDView totalValues: values];
} version: @"1"];
TDQueryOptions options = kDefaultTDQueryOptions;
View
24 TouchDB.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 2700BC5114B3864B00B5B297 /* HTTPFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156914ACEFC90065964D /* HTTPFileResponse.m */; };
270B3DFD1489359000E0A926 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270B3DFC1489359000E0A926 /* SenTestingKit.framework */; };
270B3DFE1489359000E0A926 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27C70697148864BA00F0F099 /* Cocoa.framework */; };
270B3E011489359000E0A926 /* TouchDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270B3DEA1489359000E0A926 /* TouchDB.framework */; };
@@ -91,11 +92,7 @@
275315E814ACF1130065964D /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753155A14ACEFC90065964D /* HTTPConnection.m */; };
275315E914ACF1130065964D /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753155D14ACEFC90065964D /* HTTPMessage.m */; };
275315EA14ACF1130065964D /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156014ACEFC90065964D /* HTTPServer.m */; };
- 275315EB14ACF1130065964D /* HTTPAsyncFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156314ACEFC90065964D /* HTTPAsyncFileResponse.m */; };
275315EC14ACF1130065964D /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156514ACEFC90065964D /* HTTPDataResponse.m */; };
- 275315ED14ACF1130065964D /* HTTPDynamicFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156714ACEFC90065964D /* HTTPDynamicFileResponse.m */; };
- 275315EE14ACF1130065964D /* HTTPFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156914ACEFC90065964D /* HTTPFileResponse.m */; };
- 275315EF14ACF1130065964D /* HTTPRedirectResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156B14ACEFC90065964D /* HTTPRedirectResponse.m */; };
275315F014ACF1130065964D /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156D14ACEFC90065964D /* WebSocket.m */; };
275315F114ACF1130065964D /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 275315A914ACF00B0065964D /* GCDAsyncSocket.m */; };
275315F214ACF1130065964D /* DDLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 275315B814ACF0330065964D /* DDLog.m */; };
@@ -1510,24 +1507,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 275315E014ACF0A20065964D /* TDListener.m in Sources */,
+ 2753160D14ACFC2A0065964D /* TDHTTPConnection.m in Sources */,
+ 27E11F1114AD15940006B340 /* TDHTTPResponse.m in Sources */,
275315E414ACF1130065964D /* DDData.m in Sources */,
+ 275315F214ACF1130065964D /* DDLog.m in Sources */,
275315E514ACF1130065964D /* DDNumber.m in Sources */,
275315E614ACF1130065964D /* DDRange.m in Sources */,
+ 275315F114ACF1130065964D /* GCDAsyncSocket.m in Sources */,
+ 275315EA14ACF1130065964D /* HTTPServer.m in Sources */,
+ 275315E914ACF1130065964D /* HTTPMessage.m in Sources */,
275315E714ACF1130065964D /* HTTPAuthenticationRequest.m in Sources */,
275315E814ACF1130065964D /* HTTPConnection.m in Sources */,
- 275315E914ACF1130065964D /* HTTPMessage.m in Sources */,
- 275315EA14ACF1130065964D /* HTTPServer.m in Sources */,
- 275315EB14ACF1130065964D /* HTTPAsyncFileResponse.m in Sources */,
275315EC14ACF1130065964D /* HTTPDataResponse.m in Sources */,
- 275315ED14ACF1130065964D /* HTTPDynamicFileResponse.m in Sources */,
- 275315EE14ACF1130065964D /* HTTPFileResponse.m in Sources */,
- 275315EF14ACF1130065964D /* HTTPRedirectResponse.m in Sources */,
+ 2700BC5114B3864B00B5B297 /* HTTPFileResponse.m in Sources */,
275315F014ACF1130065964D /* WebSocket.m in Sources */,
- 275315F114ACF1130065964D /* GCDAsyncSocket.m in Sources */,
- 275315F214ACF1130065964D /* DDLog.m in Sources */,
- 275315E014ACF0A20065964D /* TDListener.m in Sources */,
- 2753160D14ACFC2A0065964D /* TDHTTPConnection.m in Sources */,
- 27E11F1114AD15940006B340 /* TDHTTPResponse.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
6 TouchDB.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme
@@ -61,10 +61,14 @@
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
- argument = "-LogTDDatabase YES"
+ argument = "-LogTDListener YES"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
+ argument = "-LogTDDatabase YES"
+ isEnabled = "NO">
+ </CommandLineArgument>
+ <CommandLineArgument
argument = "-LogView YES"
isEnabled = "NO">
</CommandLineArgument>

0 comments on commit e23509c

Please sign in to comment.