-
Notifications
You must be signed in to change notification settings - Fork 51
Getting Started
This section covers creating a standalone background app (a launchd daemon) using Criollo.
TL;DR; You can just proceed to get the skeleton project from here.
Since the final build product is an OS X application, we will start with a Cocoa Application template project.[^An Xcode project template is in the pipeline, but I really haven’t gotten around to it.] The demo below is in Objective-C but it applies to Swift as well.
Check out the HelloWorld-Swift and HelloWorld-ObjC examples for a basic project.
This will give us a really good starting point. None of the options should be selected.
There is some ‘excess fat’ in the standard cocoa application template - which we really don’t need. So, let’s get rid of it. Delete Assets.xcassets
and MainMenu.xib
. We are not really using those. Of course you can leave Assets.xcassets
.
Once the basic cleanup of the project is done, we must add Criollo and configure the project to use it.
Open Terminal and cd
to your project folder, and type in
pod init
This will create a file called Podfile
. Open it in your favourite text editor and modify it so that it looks like this:
platform :osx, '10.10'
use_frameworks!
target ‘HelloWorld’ do
pod 'Criollo', '~> 0.5’
end
After that is done, in Terminal, run:
pod install
This will install Criollo and its dependencies and create a .xcworkspace
bundle. You should use this file and not the .xcodeproj
file.
Open the workspace in Xcode, and build.
We have to make a small change to the info.plist
file. Normally, the Principal Class
of a Cocoa application is NSApplication
. NSApplication takes care of a bunch of stuff like setting up a RunLoop
, connecting to the graphics server, handling application events etc.
In the case of a Criollo application, these tasks are performed by the CRApplication
class. We must tell the bundle that its Principal Class
is CRApplication
.
Here’s how to do that:
- Open
HelloWorld.xcworkspace
in Xcode, then locate and select theInfo.plist
file, in the Project navigator. - In the editor window, locate the key labeled
Principal class
(orNSPrincipalClass
if you’re showing raw key/values). - Replace
NSApplication
withCRApplication
.
A typical Cocoa application will call the NSApplicationMain
function to set everything up. A Criollo application does this with the CRApplicationMain
function. This is located in the main.m
file.
A typical main.m
file looks like this:
#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
return NSApplicationMain(argc, argv);
}
In a Criollo app, the main.m file should look like this, assuming that AppDelegate
is the application delegate class.
#import <Criollo/Criollo.h>
#import "AppDelegate.h"
int main(int argc, const char * argv[]) {
return CRApplicationMain(argc, argv, [AppDelegate new]);
}
Eagle-eyed readers will observe that CRApplicationMain
has one extra parameter. As you have guessed, this is the application delegate. Feel free to check out the code of CRApplicationMain
.
Edit the main.m
file and make the changes as indicated above.
At this point Xcode will display a warning about assigning AppDelegate *
to id<CRApplicationDelegate>
. We’ll take care of that next.
As in the case of Cocoa, Criollo defines the CRApplicationDelegate
protocol[^Again apologies for the lack of documentation].
The file should look like this:
#import <Criollo/Criollo.h>
@interface AppDelegate : NSObject <CRApplicationDelegate>
@end
The only two changes should be importing the Criollo header and conforming to CRApplicationDelegate
instead of NSApplicationDelegate
.
Cocoa apps do more than we need. The main thing is to remove the IBOutlet
declaration, since we are not using Interface Builder to design our UI (if any).
Look for the line @property (weak) IBOutlet NSWindow *window;
and remove it. Everything else stays the same. The file should look like this:
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
@end
You’ll notice a few familiar methods; they do the exact same thing as in Cocoa or CocoaTouch.
You can download this skeleton project here, for future use.
At this point the app should build, start and hang around, doing absolutely nothing.
The next step is to spin-up a server and start serving some content.
Depending on which way you need to go with your project, you can use either of the two (HTTP/FastCGI). This is not a decision you have to make at the start of development since the two classes are perfectly compatible. In case you need to switch just change the class you are instantiating.
First, let’s declare the server object as a property of the AppDelegate, for the purposes of retaining a strong reference to it, just to prevent ARC from prematurely dealloc
-ing as soon as we exit applicationDidFinishLaunching:
.
@property (nonatomic, strong, nonnull) CRServer* server;
After that, inside applicationDidFinishLaunching:
we initialize the server and start listening.
self.server = [[CRHTTPServer alloc] init];
[self.server startListening];
Criollo will start an HTTP server that is bound to all interfaces on port 10781
. The server is accessible at http://localhost:10781/.
At this point, the server will return a 404 Not Found response, because there are no routes defined as yet.
The info displayed to the client is provided by the built-in error handling block. The code for this is found here.
The basic building block of Criollo’s routing system is a CRRouteBlock
.
typedef void(^CRRouteBlock)(CRRequest* _Nonnull request, CRResponse* _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler);
These blocks are added to paths according to HTTP request methods and they get executed in the order in which they are added to the route.
CRRouter
offers several ways of adding a CRRouteBlock
to a route.
For the purpose of this guide we will add a block that says “Hello World”, to the path “/", for the HTTP method “GET”.
[self.server get:@"/" block:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler) {
[response send:@"Hello world!"];
completionHandler();
}];
Using send:
, or any of the send
family of functions, causes the response to end. Subsequent calls to the write
or send
family of functions will throw an NSInternalInconsistencyException
. Check out the rest of the CRResponse API.
At the end of the work that this block will perform, call completionHandler()
in order to proceed with the execution of the rest of the blocks added to the current route.
Typically a middleware is a block that gets executed on a series of paths and is not the end-point of the route.
- (void)add:(CRRouteBlock _Nonnull)block;
Calling this method of CRRouter
will cause the block
to be added to all paths on all HTTP methods. Of course the same result could be achieved by calling any other versions of this method with the path
and HTTPMethod
parameters set to nil
.
Let’s add a middleware that will set the Server
HTTP header on all responses. Useful for bragging rights.
We will add this before the “Hello world” block, as that one finishes the response.
[self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler) {
[response setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:@"Server"];
completionHandler();
}];
Even though the “Hello world” block finishes the response, additional blocks can be added to the route. The only restriction is that they cannot alter the response. It has already been sent.
Let’s add a block that simply does some NSLog
-ing.
[self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler) {
NSUInteger statusCode = request.response.statusCode;
NSString* contentLength = [request.response valueForHTTPHeaderField:@"Content-Length"];
NSString* userAgent = request.env[@"HTTP_USER_AGENT"];
NSString* remoteAddress = request.connection.remoteAddress;
NSLog(@"%@ %@ - %lu %@ - %@", remoteAddress, request, statusCode, contentLength ? : @"-", userAgent);
completionHandler();
}];
All this block does is log some nice info about the request and response to the console.
By this point we have a server running at http://localhost:10781/, that sets a Server
header on all responses, says “Hello world” and then NSLog
s what it has just done.
The AppDelegate.m file should look like this:
#import "AppDelegate.h"
@interface AppDelegate () <CRServerDelegate>
@property (nonatomic, strong, nonnull) CRServer* server;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.server = [[CRHTTPServer alloc] init];
[self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler) {
[response setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:@"Server"];
completionHandler();
}];
[self.server get:@"/" block:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler) {
[response sendString:@"Hello world!"];
completionHandler();
}];
[self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler) {
NSUInteger statusCode = request.response.statusCode;
NSString* contentLength = [request.response valueForHTTPHeaderField:@"Content-Length"];
NSString* userAgent = request.env[@"HTTP_USER_AGENT"];
NSString* remoteAddress = request.connection.remoteAddress;
NSLog(@"%@ %@ - %lu %@ - %@", remoteAddress, request, statusCode, contentLength ? : @"-", userAgent);
completionHandler();
}];
[self.server startListening];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
@end