Skip to content

Commit

Permalink
Objective-C view support for Couchbase Mobile iOS
Browse files Browse the repository at this point in the history
This is a squashed commit of all the work done on other
branches by Jonathon Mah & me.

Change-Id: I0b9d020d57502b2ea46b695d9788e42ceb7225fb
Reviewed-on: http://review.couchbase.org/10023
Reviewed-by: Jonathon Mah <me@jonathonmah.com>
Reviewed-by: Chris Anderson <jchris@couchbase.com>
Tested-by: Jens Alfke <jens@couchbase.com>
  • Loading branch information
snej committed Oct 25, 2011
1 parent b0c2357 commit 8ea7a75
Show file tree
Hide file tree
Showing 14 changed files with 1,497 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/couchdb/couch_app_server_objc.erl
@@ -0,0 +1,50 @@
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.

-module(couch_app_server_objc).

-export([get_server/3, ret_server/1]).
-export([show_doc/5, validate_update/6, filter_docs/5, filter_view/4]).
-export([list_start/5, list_row/3, list_end/2, update_doc/5]).

-include("couch_db.hrl").

get_server(_Arg, DDoc, DDocKey) ->
objc_dispatch:app_create(DDoc, DDocKey).

ret_server(_) ->
ok.

show_doc(Ctx, DDocId, ShowName, Doc, Req) ->
objc_dispatch:app_show_doc(Ctx, DDocId, ShowName, Doc, Req).

list_start(Ctx, DDocId, ListName, Head, Req) ->
{message, [Chunks, Resp]} = objc_dispatch:app_list_start(Ctx, DDocId, ListName, Head, Req),
{ok, Chunks, Resp}.

list_row(Ctx, DDocId, Row) ->
objc_dispatch:app_list_row(Ctx, DDocId, Row).

list_end(Ctx, DDocId) ->
objc_dispatch:app_list_end(Ctx, DDocId).

update_doc(Ctx, DDocId, UpdateName, Doc, Req) ->
objc_dispatch:app_update_doc(Ctx, DDocId, UpdateName, Doc, Req).

validate_update(Ctx, DDocId, EditDoc, DiskDoc, Context, SecObj) ->
objc_dispatch:app_validate_update(Ctx, DDocId, EditDoc, DiskDoc, Context, SecObj).

filter_docs(Ctx, DDocId, FilterName, Docs, Req) ->
objc_dispatch:app_filter_docs(Ctx, DDocId, FilterName, Docs, Req).

filter_view(Ctx, DDocId, ViewName, Docs) ->
objc_dispatch:app_filter_view(Ctx, DDocId, ViewName, Docs).
71 changes: 71 additions & 0 deletions src/couchdb/couch_view_server_objc.erl
@@ -0,0 +1,71 @@
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couch_view_server_objc).

-export([is_lightweight/0, add_view_deps/1]).
-export([get_server/3, ret_server/1]).
-export([map/2, reduce/3, rereduce/3]).

-include("couch_db.hrl").

is_lightweight() ->
true.

add_view_deps(View) ->
% Catch any error here; we don't really care what it is, only
% that there is an error (so we don't correspond to any other
% pre-computed views). The underlying error will be hit when
% (and if) the view is actually built.
ObjCVers = try objc_dispatch:query_view_version(View#view.def)
catch
_ -> error
end,
[View, ObjCVers].

get_server(_Arg, Maps, ReduceStruct) ->
% ReduceStruct is [[ViewId, [RedDef, ...]], [ViewId, [...], ...].
% Turn this into [RedDef, ...] to decode, and a map of [{ViewId, [3, 4]}, ...]
% where [3,4] are indexes (0-based) into the array of RedDefs.
{RevIndexMap, RevAllRedDefs, _} = lists:foldl(fun([ViewId, RedDefs], {IdIndexAssoc, RedDefAcc, NextIndex}) ->
{NewRedDefs, Indexes, NewNextIndex} = lists:foldl(fun(RedDef, {RedDefs, Indexes, Index}) ->
{[RedDef | RedDefs], [Index | Indexes], Index + 1}
end, {RedDefAcc, [], NextIndex}, RedDefs),
{[{ViewId, Indexes} | IdIndexAssoc], NewRedDefs, NewNextIndex}
end, {[], [], 0}, ReduceStruct),
AllRedDefs = lists:reverse(RevAllRedDefs),
RedIndexMap = [{ViewId, lists:reverse(RevIndexes)} || {ViewId, RevIndexes} <- RevIndexMap],
{ok, QueueRef} = objc_dispatch:create(<<"org.couchdb.objc_view">>, Maps, AllRedDefs),
{ok, {QueueRef, RedIndexMap}}.

ret_server(_) ->
ok.

map({QueueRef, _RedIndexMap}, Docs) ->
Results = lists:map(fun(Doc) ->
JsonDoc = couch_doc:to_json_obj(Doc, []),
{ok, EmitsPerView} = objc_dispatch:map(QueueRef, JsonDoc),
lists:map(fun(EmitListItem) ->
[list_to_tuple(EmitPair) || EmitPair <- EmitListItem]
end, EmitsPerView)
end, Docs),
{ok, Results}.

reduce({QueueRef, RedIndexMap}, ViewId, KVs) ->
Indexes = proplists:get_value(ViewId, RedIndexMap),
{Keys, Vals} = lists:foldl(fun([K, V], {KAcc, VAcc}) ->
{[K | KAcc], [V | VAcc]}
end, {[], []}, KVs),
objc_dispatch:reduce(QueueRef, Indexes, Keys, Vals, false).

rereduce({QueueRef, RedIndexMap}, ViewId, Vals) ->
Indexes = proplists:get_value(ViewId, RedIndexMap),
objc_dispatch:reduce(QueueRef, Indexes, null, Vals, true).
30 changes: 30 additions & 0 deletions src/objc-dispatch/c_src/CouchbaseAppServer.h
@@ -0,0 +1,30 @@
//
// CouchbaseAppServer.h
// iErl14
//
// Created by Jens Alfke on 10/6/11.
// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "CouchbaseCallbacks.h"
#include "erl_nif.h"

@interface CouchbaseAppServer : NSObject
{
ErlNifEnv* _env;
NSDictionary* _designDoc;
CouchValidateUpdateBlock _validateUpdateBlock;
}

- (id) initWithDesignDoc: (NSDictionary*)designDoc;

@property ErlNifEnv* env;

- (BOOL) validateUpdate: (ERL_NIF_TERM)updatedDoc
ofDocument: (ERL_NIF_TERM)currentRevision
context: (ERL_NIF_TERM)context
security: (ERL_NIF_TERM)security
error: (NSDictionary**)outError;

@end
156 changes: 156 additions & 0 deletions src/objc-dispatch/c_src/CouchbaseAppServer.m
@@ -0,0 +1,156 @@
//
// CouchbaseAppServer.m
// iErl14
//
// Created by Jens Alfke on 10/6/11.
// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
//

#import "CouchbaseAppServer.h"
#import "term_to_objc.h"


@interface CouchbaseValidationContext : NSObject <CouchbaseValidationContext>
{
@private
ErlNifEnv* _env;
ERL_NIF_TERM _currentRevisionTerm, _contextTerm, _securityTerm;
NSDictionary* _currentRevision;
NSDictionary* _contextDict;
NSDictionary* _security;
NSString* _errorType, *_errorMessage;
}
- (id) initWithEnv: (ErlNifEnv*)env
currentRevision: (ERL_NIF_TERM)currentRevision
context: (ERL_NIF_TERM)context
security: (ERL_NIF_TERM)security;
@end


@implementation CouchbaseAppServer

- (id) initWithDesignDoc: (NSDictionary*)designDoc {
self = [super init];
if (self) {
NSParameterAssert(designDoc);
_designDoc = [designDoc copy];
}
return self;
}

- (void)dealloc {
[_validateUpdateBlock release];
[_designDoc release];
[super dealloc];
}


@synthesize env = _env;


- (BOOL) validateUpdate: (ERL_NIF_TERM)documentTerm
ofDocument: (ERL_NIF_TERM)currentRevisionTerm
context: (ERL_NIF_TERM)contextTerm
security: (ERL_NIF_TERM)securityTerm
error: (NSDictionary**)outError
{
if (!_validateUpdateBlock) {
NSString* key = [_designDoc objectForKey: @"validate_doc_update"];
if ([key isKindOfClass: [NSString class]])
_validateUpdateBlock = [[[CouchbaseCallbacks sharedInstance]
validateUpdateBlockForKey: key] copy];

if (!_validateUpdateBlock)
[NSException raise: NSInternalInconsistencyException
format: @"Unregistered native validate_doc_update function '%@'", key];
}

NSDictionary* document = term_to_nsdict(_env, documentTerm);
if (!document)
return NO;

CouchbaseValidationContext* context = [[CouchbaseValidationContext alloc]
initWithEnv: _env
currentRevision: currentRevisionTerm
context: contextTerm
security: securityTerm];
[context autorelease];

if (_validateUpdateBlock(document, context))
return YES;

*outError = [NSDictionary dictionaryWithObject: context.errorMessage forKey: context.errorType];
NSLog(@"Doc failed validation with error {%@: %@}", context.errorType, context.errorMessage);
return NO;
}

@end



@implementation CouchbaseValidationContext


- (id) initWithEnv: (ErlNifEnv*)env
currentRevision: (ERL_NIF_TERM)currentRevision
context: (ERL_NIF_TERM)context
security: (ERL_NIF_TERM)security
{
self = [super init];
if (self) {
NSParameterAssert(env);
_env = env;
_currentRevisionTerm = currentRevision;
_contextTerm = context;
_securityTerm = security;
_errorType = [@"forbidden" retain];
_errorMessage = [@"invalid document" retain];
}
return self;
}

- (void)dealloc {
[_errorType release];
[_errorMessage release];
[_currentRevision release];
[_contextDict release];
[_security release];
[super dealloc];
}

- (NSDictionary*) currentRevision {
if (!_currentRevision)
_currentRevision = [term_to_nsdict(_env, _currentRevisionTerm) retain];
return _currentRevision;
}

- (NSDictionary*) contextDict {
if (!_contextDict)
_contextDict = [term_to_nsdict(_env, _contextTerm) retain];
return _contextDict;
}

- (NSString*) databaseName {
return [self.contextDict objectForKey: @"db"];
}

- (NSString*) userName {
id user = [self.contextDict objectForKey: @"name"];
if (![user isKindOfClass: [NSString class]]) // anonymous user will be a NSNull object
return nil;
return user;
}

- (BOOL) isAdmin {
return [[self.contextDict objectForKey: @"roles"] containsObject: @"_admin"];
}

- (NSDictionary*) security {
if (!_security)
_security = [term_to_nsdict(_env, _securityTerm) retain];
return _security;
}

@synthesize errorType = _errorType, errorMessage = _errorMessage;

@end
84 changes: 84 additions & 0 deletions src/objc-dispatch/c_src/CouchbaseCallbacks.h
@@ -0,0 +1,84 @@
//
// CouchbaseCallbacks.h
// iErl14
//
// Created by Jens Alfke on 10/3/11.
// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
//

#import <Foundation/Foundation.h>
@protocol CouchbaseValidationContext;


typedef void (^CouchEmitBlock)(id key, id value);

/** A "map" function called when a document is to be added to a view.
@param doc The contents of the document being analyzed.
@param emit A block to be called to add a key/value pair to the view. Your block can call zero, one or multiple times. */
typedef void (^CouchMapBlock)(NSDictionary* doc, CouchEmitBlock emit);

/** A "reduce" function called to summarize the results of a view.
@param keys An array of keys to be reduced.
@param values A parallel array of values to be reduced, corresponding one-to-one with the keys.
@param rereduce YES if the input keys and values are the results of previous reductions.
@return The reduced value; almost always a scalar or small fixed-size object. */
typedef id (^CouchReduceBlock)(NSArray* keys, NSArray* values, BOOL rereduce);

/** Called to validate a document before it's added to the database.
@param doc The submitted document contents.
@param context Lets the block access relevant information and specify an error message.
@return YES to accept the document, NO to reject it. */
typedef BOOL (^CouchValidateUpdateBlock)(NSDictionary* doc,
id<CouchbaseValidationContext> context);

/** Central per-process registry for native design-document functions.
Associates a key (a unique ID stored in the design doc as the "source" of the function)
with a C block.
This class is thread-safe. */
@interface CouchbaseCallbacks : NSObject
{
NSArray* _registries;
}

+ (CouchbaseCallbacks*) sharedInstance;

- (NSString*) generateKey;

- (void) registerMapBlock: (CouchMapBlock)block forKey: (NSString*)key;
- (void) registerReduceBlock: (CouchReduceBlock)block forKey: (NSString*)key;
- (void) registerValidateUpdateBlock: (CouchValidateUpdateBlock)block forKey: (NSString*)key;

- (CouchMapBlock) mapBlockForKey: (NSString*)key;
- (CouchReduceBlock) reduceBlockForKey: (NSString*)key;
- (CouchValidateUpdateBlock) validateUpdateBlockForKey: (NSString*)key;

@end


/** Context passed into a CouchValidateUpdateBlock. */
@protocol CouchbaseValidationContext <NSObject>

/** The contents of the current revision of the document, or nil if this is a new document. */
@property (readonly) NSDictionary* currentRevision;

/** The name of the database being updated. */
@property (readonly) NSString* databaseName;

/** The name of the logged-in user, or nil if this is an anonymous request. */
@property (readonly) NSString* userName;

/** Does the user have admin privileges?
(If the database is in the default "admin party" mode, this will be YES even when the userName is nil.) */
@property (readonly) BOOL isAdmin;

/** The database's security object, which assigns roles and privileges. */
@property (readonly) NSDictionary* security;

/** The type of error to report, if the validate block returns NO.
The default value is "forbidden", which will result in an HTTP 403 status. */
@property (copy) NSString* errorType;

/** The error message to return in the HTTP response, if the validate block returns NO.
The default value is "invalid document". */
@property (copy) NSString* errorMessage;
@end

0 comments on commit 8ea7a75

Please sign in to comment.