Monday, June 29, 2015

Mock almost any web service with these 100 lines of ruby code

Many applications are now dependent upon web services to provide much of their functionality.  Testing applications and their interactions with these web services can be pretty hard to automate without a mock service.

I recently needed to write some unit tests for a bit of code that communicated with a web service, and so I started reading up on Sinatra.

It turns out that Sinatra makes it very simple to generate just about any web service.  It is especially trivial to mock data for a given web service end point.  Take this example right out of the Sinatra README:

get '/hello/:name' do
  # matches "GET /hello/foo" and "GET /hello/bar"
  # params['name'] is 'foo' or 'bar'
  "Hello #{params['name']}!"
end

This seemed awesome, but I realized that I had maybe 50 endpoints that I wanted to mock.  I guess this still isn't too bad, but I really wanted to generalize my ruby code in such a way that it could mock any endpoint without code modification.

So I wrote a Sinatra service that has only 4 end points using splat captures (*), which is basically a wildcard.  I made one route for each of the main HTTP verbs (POST,PUT,GET,DELETE), and had each route match ANY endpoint that matched its verb, meaning:

get '*' do     ...
put '*' do     ...
post '*' do    ...
delete '*' do  ...

I then wrote each route to mirror a "fixtures" directory on the filesystem.

For example, say I had an endpoint to some API for getting user information:

GET /companies/company1/users/jdoe

The get '*' route handles this and browses the file on the filesystem at ./fixtures/companies/company1/users/jdoe.json, and returns the contents of this file in the body of the response:

get '*' do
    path = Pathname.new("#{File.dirname(__FILE__)}/#{FIXTURESDIR}#{params[:splat].first()}")
    pathPlusJson = Pathname.new("#{path}.json")
    if path.directory?
        response = get_directory_contents_array(path.to_path)
        return response.to_json
    elsif pathPlusJson.exist? and pathPlusJson.file?
        response = JSON.parse(IO.read(pathPlusJson.to_path))
        return response.to_json
    else
        return create_response( {"error" => "Not Found" }.to_json, 404 )
    end
end

If I wanted to iterate all users, I could instead request a get on a directory instead of a file:

 GET /companies/company1/users

Now the get '*' route browses to the directory and iterates through all the ".json" files returning an array of JSON encoded user objects in the body.

If I want to add a new user I can simply do a POST:

POST /companies/company1/users/dsmith

with some JSON in the body, and this JSON will be written to ./fixtures/companies/company1/dsmith.json

Delete also works as you might expect, deleting the file in the fixtures directory.

All of this can be done with very little code thanks to the domain specific nature of Sinatra.  The full ruby code can be found in one of my GitHub Gists's:

https://gist.github.com/jhnbwrs/6e940679a3dd1a23cbc0

Now, if you are unit testing a ruby application you can stub any requests to your api endpoint using webmock:

# spec/spec_helper.rb RSpec.configure 
do |config| 
  config.before(:each) do 
    stub_request(:any, /api.company1.com/).to_rack(JSONFileServer) 
  end 
end

If you aren't unit testing a ruby application, you should still be able to fire up this service during the unit test phase of your build, and redirect your API requests to localhost:4567.




Wednesday, June 17, 2015

Adding a right click context menu to finder from an OSX application

Perhaps you have an application that you think would really really benefit from an additional finder menu.  You would really like to show this menu under some circumstance.  Perhaps you want it to show for a specific kind of file, or a when a file is in a specific state.

In the next few paragraphs, I am going to discuss what you can and cannot easily add to the finder menu, and I am providing access to code for a project that does this:

https://github.com/jhnbwrs/Base64Project

You can also download the pre-built application from the AppStore if you want see how it works without compiling some code:

https://itunes.apple.com/us/app/base64anywhere/id640841706?mt=12

Ok, now for the bad news:

In reality, you cannot add anything to finder.  You can get things added to the finder context menu, but it is more like you are kindly asking OSX to add the items.  You have a little bit of control, but not a lot, and the control you do have really involves telling OSX to display a service for a specific kind of content.  If you want more control that isn't based upon content type (say the state of the file), it is basically impossible since the cocoa re-write of Finder in 10.6.

If you want to provide a service based upon a specific kind of content (like files, or text), then the good news is that this is very very easy.

Getting content to show up in finder is all about something called NSServices, and you can find the implementation guide here:

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/SysServices/introduction.html#//apple_ref/doc/uid/10000101-SW1

So let's use our example code/application to demonstrate how easily this can be done.  The application we are using for an example base64 encodes files and text, as well as decoding base64 encoded text.  This sort of application is so much more useful if you can simple right click on a file and encode it, or perhaps highlight some base64encoded text (perhaps found while browsing the web) and right click to decode it

If this is what we want, all we have to do is modify our application's info.plist telling OSX that we want to provide a service or two for text and/or files.  Let's look first at the service registration for decoding base64encoded text:


If you were looking at just then entry in a text editor, it would look like this:

     <key>NSServices</key> 
     <array>
          <dict>
               <key>NSMenuItem</key>
               <dict>
                    <key>default</key>
                    <string>Base64Decode</string>
               </dict>

               <key>NSRequiredContext</key>
               <dict/>
               <key>NSMessage</key>
               <string>DecodeText</string>
               <key>NSSendTypes</key>
               <array>
                    <string>public.text</string>
               </array>
          </dict>
     </array>


So we have 4 important items here:

1. NSMenuItem - The "default" key describes the text that will show up in your right click menu.  In this case "Base64Decode"

2. NSMessage - This message that gets sent to you application when this service is called.  This is where you code begins executing when the menu item above is clicked upon.  This must be defined on a class that is registered as the "ServiceProvider" for your application.  In the case of Base64Anywhere, I registered in application did finish launching.


        - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
        {
            ServiceProvider* provider = [[ServiceProvider allocinit];
            [NSApp setServicesProvider:provider]
        }

Then I defined this the DecodeText message receiver on the service provider class as follows:

     - (void) DecodeText: (NSPasteboard*) pasteboard : (NSString*) error;

3. NSRequiredContext - This allows you to restrict in some ways where or when your service is shown.  For example, you could restrict a text service to only show up for text that conforms to a filepath.  Even if you are not specifying restrictions, you still must include the NSRequiredContext key with an empty dictionary.  If you do not, your service will not show up.

4. NSSendTypes - This defines what content type your service shows up for.  In this example we are using the Apple defined UTI for "text".  You can specify a service for any number of Apple defined UTIs.  A list can be found here:



If you want to provide a service for a specific type of file not listed here (maybe because your application created the file type), you can create and register your own UTI.  Once it is defined you can include it in the NSSendTypes, so that a menu item only shows up for your custom file type. Details on how to register a new UTI can be found here:


If your service needs to contextually return something, you may also want to define the NSReturnTypes for your service.  In the base64 encoding example, we might want to highlight some text in Xcode and replace it with the base64 encoded equivalent.  We can do this by specifying the appropriate return type for our service.

                        <key>NSReturnTypes</key>
                        <array>
                                <string>NSStringPboardType</string>
                        </array>

The return type is especially useful if we are creating a .service bundle (as opposed to .app bundle).  This service can then actually perform an action retuning some value into an existing UI without needing any UI of its own.

Hopefully this helps you get started adding services to your own application.  Leave comments below!

--
John