Swift playground action feature with dynamic library (#23)

* import form swift3action from OPenWhisk for local testing

* test swift snippets for playground REPL

* modify Package.swift to build dynamic libraries - currently only for
SwiftyJSON as that is the only one without issues ….

* build and run docker image with REPL playground inside

* tests and current status

* how output in README

* added license to test files
diff --git a/swift-playground-action/Dockerfile b/swift-playground-action/Dockerfile
new file mode 100644
index 0000000..2133673
--- /dev/null
+++ b/swift-playground-action/Dockerfile
@@ -0,0 +1,16 @@
+FROM swift3action:base
+
+ENV DEBIAN_FRONTEND noninteractive
+
+ENV PYTHONIOENCODING utf-8
+
+RUN sudo apt-get -y install libpython2.7
+
+COPY add_dylib.py /
+
+RUN /add_dylib.py
+
+WORKDIR /swift3Action/spm-build/
+RUN /bin/bash -c "/usr/bin/swift build"
+
+CMD ["/bin/bash", "-c", "cd /swift3Action && PYTHONIOENCODING='utf-8' python -u swift3runner.py"]
diff --git a/swift-playground-action/README.md b/swift-playground-action/README.md
new file mode 100644
index 0000000..2d7d84e
--- /dev/null
+++ b/swift-playground-action/README.md
@@ -0,0 +1,77 @@
+# Enabling OpenWhisk Swift3 Action to use REPL PLayground
+
+## Test
+
+```
+./build.sh
+./run.sh
+```
+
+Then inside container run basic test
+
+```
+time /usr/bin/swift /share/test0.swift
+Hello World
+```
+
+
+and using dynamic library inside REPL calling SwiftyJSON:
+
+```
+time swift -I/swift3Action/spm-build/.build/debug -L /swift3Action/spm-build/.build/debug -lSwiftyJSON /share/test1.swift
+Hello Joe Doe
+```
+
+## Current issues - unable to build dynamic version of OpenSSL library
+
+Unable to build OpenSSL - to reproduce install editor
+
+```
+apt-get -y install nano emacs
+```
+
+Modify Package.swift
+
+```
+cd /swift3Action/spm-build/
+emacs Packages/OpenSSL-0.2.2/Package.swift
+```
+
+to add
+
+```
+products.append(Product(name: "OpenSSL", type: .Library(.Dynamic), modules: "openssl"))
+```
+
+and then run
+
+```
+root@87f331718d1e:/swift3Action/spm-build# swift build
+error: the product named OpenSSL references a module that could not be found: openssl
+fix: reference only valid modules from the product
+```
+
+
+Using
+
+```
+products.append(Product(name: "OpenSSL", type: .Library(.Dynamic), modules: "OpenSSL"))
+```
+
+leads to
+
+```
+root@87f331718d1e:/swift3Action/spm-build# swift build
+Linking ./.build/debug/libOpenSSL.so
+<unknown>:0: error: no input files
+<unknown>:0: error: build had 1 command failures
+error: exit(1): /usr/bin/swift-build-tool -f /swift3Action/spm-build/.build/debug.yaml
+```
+
+
+Optimally this should work ...
+
+```
+time swift -I/swift3Action/spm-build/.build/debug -L /swift3Action/spm-build/.build/debug -lSwiftyJSON -lOpenSSL /share/test2.swift
+<unknown>:0: error: missing required module 'OpenSSL'
+``` 
diff --git a/swift-playground-action/add_dylib.py b/swift-playground-action/add_dylib.py
new file mode 100755
index 0000000..2f27b50
--- /dev/null
+++ b/swift-playground-action/add_dylib.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+#
+# Copyright 2015-2016 IBM Corporation
+#
+# 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.
+#
+
+
+import os
+import re
+import sys
+import json
+
+# 
+# find all paclages
+# add dylib statements cusotmized ot package name
+# rebuild all packages
+
+def procesPackageFile(packageName, path):
+    stmt = '\n\nproducts.append(Product(name: "{packageName}", type: .Library(.Dynamic), modules: "{packageName}"))'.format(packageName=packageName)
+    # only patch select packages
+    if packageName == 'SwiftyJSON' :
+        print(packageName, path, stmt)
+        with open(path, "a") as myfile:
+            myfile.write(stmt + '\n')
+
+
+
+def main():
+    print("hello friend")
+    for root, dirs, files in os.walk("/swift3Action/spm-build/Packages/"):
+        for f in files:
+            if f.endswith("Package.swift"):
+                fullPath = os.path.join(root, f)
+                parentDir = os.path.split(fullPath)[0]
+                baseDir = os.path.basename(parentDir)
+                # remove version from SwiftyJSON-14.2.0
+                m = re.match("(.*)-[\d+\.]+", baseDir)
+                packageName = m.group(1)
+                #print("baseDir="+baseDir+" packageName="+packageName)
+                procesPackageFile(packageName, fullPath)
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/swift-playground-action/build.sh b/swift-playground-action/build.sh
new file mode 100755
index 0000000..9fa87e1
--- /dev/null
+++ b/swift-playground-action/build.sh
@@ -0,0 +1,5 @@
+# build docker images
+echo Building base image from OpenWhisk Swift3 Action
+(cd swift3Action; docker build -t swift3action:base .)
+echo Building Swift Playground enabled version of  OpenWhisk Swift3 Action
+docker build -t swift3action:playground .
diff --git a/swift-playground-action/run.sh b/swift-playground-action/run.sh
new file mode 100755
index 0000000..beb65d3
--- /dev/null
+++ b/swift-playground-action/run.sh
@@ -0,0 +1,6 @@
+
+
+# Need to use --privileged=true or REPL fails see:
+# https://bugs.swift.org/browse/SR-54
+
+docker run --privileged=true -it -v  $(pwd):/share  swift3action:playground /bin/bash
diff --git a/swift-playground-action/swift3Action/Dockerfile b/swift-playground-action/swift3Action/Dockerfile
new file mode 100644
index 0000000..2f2fb4b
--- /dev/null
+++ b/swift-playground-action/swift3Action/Dockerfile
@@ -0,0 +1,68 @@
+# Dockerfile for swift actions, overrides and extends ActionRunner from actionProxy
+# This Dockerfile is partially based on: https://github.com/swiftdocker/docker-swift/
+FROM buildpack-deps:trusty
+
+ENV DEBIAN_FRONTEND noninteractive
+
+# Upgrade and install basic Python dependencies
+RUN apt-get -y purge \
+ && apt-get -y update \
+ && apt-get -y install --fix-missing python2.7 python-gevent python-flask \
+\
+# Upgrade and install Swift dependencies
+ && apt-get -y install --fix-missing build-essential curl wget libicu-dev \
+\
+# Install zip for compiling Swift actions
+ && apt-get -y install zip \
+\
+# Clean up
+ && apt-get clean
+
+# Install clang manually, since SPM wants at least Clang 3-6
+RUN cd / &&\
+(curl -L -k http://llvm.org/releases/3.6.2/clang+llvm-3.6.2-x86_64-linux-gnu-ubuntu-14.04.tar.xz | tar xJ) &&\
+cp -r /clang+llvm-3.6.2-x86_64-linux-gnu-ubuntu-14.04/* /usr/ &&\
+rm -rf /clang+llvm-3.6.2-x86_64-linux-gnu-ubuntu-14.04
+
+RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/clang++ 20
+RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/clang 20
+
+# Install Swift keys
+RUN wget --no-verbose -O - https://swift.org/keys/all-keys.asc | gpg --import - && \
+    gpg --keyserver hkp://pool.sks-keyservers.net --refresh-keys Swift
+
+# Install Swift Ubuntu 14.04 Snapshot
+#https://swift.org/builds/swift-3.0.1-release/ubuntu1404/swift-3.0.1-RELEASE/swift-3.0.1-RELEASE-ubuntu14.04.tar.gz
+
+ENV SWIFT_VERSION 3.0.2
+ENV SWIFT_RELEASE_TYPE RELEASE
+ENV SWIFT_PLATFORM ubuntu14.04
+
+RUN SWIFT_ARCHIVE_NAME=swift-$SWIFT_VERSION-$SWIFT_RELEASE_TYPE-$SWIFT_PLATFORM && \
+    SWIFT_URL=https://swift.org/builds/swift-$SWIFT_VERSION-$(echo "$SWIFT_RELEASE_TYPE" | tr '[:upper:]' '[:lower:]')/$(echo "$SWIFT_PLATFORM" | tr -d .)/swift-$SWIFT_VERSION-$SWIFT_RELEASE_TYPE/$SWIFT_ARCHIVE_NAME.tar.gz && \
+    echo $SWIFT_URL && \
+    wget --no-verbose $SWIFT_URL && \
+    wget --no-verbose $SWIFT_URL.sig && \
+    gpg --verify $SWIFT_ARCHIVE_NAME.tar.gz.sig && \
+    tar -xzf $SWIFT_ARCHIVE_NAME.tar.gz --directory / --strip-components=1 && \
+    rm -rf $SWIFT_ARCHIVE_NAME* /tmp/* /var/tmp/*
+
+# Add the action proxy
+RUN mkdir -p /actionProxy
+ADD actionproxy.py /actionProxy
+
+# Add files needed to build and run action
+RUN mkdir -p /swift3Action
+ADD epilogue.swift /swift3Action
+ADD buildandrecord.py /swift3Action
+ADD swift3runner.py /swift3Action
+ADD spm-build /swift3Action/spm-build
+
+
+# Build kitura net
+RUN touch /swift3Action/spm-build/main.swift
+RUN python /swift3Action/buildandrecord.py; rm /swift3Action/spm-build/.build/release/Action
+#RUN cd /swift3Action/spm-build; swift build -c release; rm /swift3Action/spm-build/.build/release/Action
+ENV FLASK_PROXY_PORT 8080
+
+CMD ["/bin/bash", "-c", "cd /swift3Action && PYTHONIOENCODING='utf-8' python -u swift3runner.py"]
diff --git a/swift-playground-action/swift3Action/README.md b/swift-playground-action/swift3Action/README.md
new file mode 100644
index 0000000..e5e454d
--- /dev/null
+++ b/swift-playground-action/swift3Action/README.md
@@ -0,0 +1,37 @@
+Skeleton for "docker actions"
+================
+
+The `dockerskeleton` base image is useful for actions that run scripts (e.g., bash, perl, python)
+and compiled binaries or, more generally, any native executable. It provides a proxy service
+(using Flask, a Python web microframework) that implements the required `/init` and `/run` routes
+to interact with the OpenWhisk invoker service. The implementation of these routes is encapsulated
+in a class named `ActionRunner` which provides a basic framework for receiving code from an invoker,
+preparing it for execution, and then running the code when required.
+
+The initialization of the `ActionRunner` is done via `init()` which receives a JSON object containing
+a `code` property whose value is the source code to execute. It writes the source to a `source` file.
+This method also provides a hook to optionally augment the received code via an `epilogue()` method,
+and then performs a `build()` to generate an executable. The last step of the initialization applies
+`verify()` to confirm the executable has the proper permissions to run the code. The action runner
+is ready to run the action if `verify()` is true.
+
+The default implementations of `epilogue()` and `build()` are no-ops and should be overridden as needed.
+The base image contains a stub added which is already executable by construction via `docker build`.
+For language runtimes (e.g., C) that require compiling the source, the extending class should run the
+required source compiler during `build()`.
+
+The `run()` method runs the action via the executable generated during `init()`. This method is only called
+by the proxy service if `verify()` is true. `ActionRunner` subclasses are encouraged to override this method
+if they have additional logic that should cause `run()` to never execute. The `run()` method calls the executable
+via a process and sends the received input parameters (from the invoker) to the action via the command line
+(as a JSON string argument). Additional properties received from the invoker are passed on to the action via
+environment variables as well. To augment the action environment, override `env()`.
+
+By convention the action executable may log messages to `stdout` and `stderr`. The proxy requires that the last
+line of output to `stdout` is a valid JSON object serialized to string if the action returns a JSON result.
+A return value is optional but must be a JSON object (properly serialized) if present.
+
+For an example implementation of an `ActionRunner` that overrides `epilogue()` and `build()` see the
+[Swift 3](../swift3Action/swift3runner.py) action proxy. An implementation of the runner for Python actions
+is available [here](../pythonAction/pythonrunner.py). Lastly, an example Docker action that uses `C` is
+available in this [example](../../sdk/docker/Dockerfile).
diff --git a/swift-playground-action/swift3Action/actionproxy.py b/swift-playground-action/swift3Action/actionproxy.py
new file mode 100644
index 0000000..c68231f
--- /dev/null
+++ b/swift-playground-action/swift3Action/actionproxy.py
@@ -0,0 +1,271 @@
+"""Executable Python script for a proxy service to dockerSkeleton.
+
+Provides a proxy service (using Flask, a Python web microframework)
+that implements the required /init and /run routes to interact with
+the OpenWhisk invoker service.
+
+The implementation of these routes is encapsulated in a class named
+ActionRunner which provides a basic framework for receiving code
+from an invoker, preparing it for execution, and then running the
+code when required.
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+"""
+
+import sys
+import os
+import json
+import subprocess
+import codecs
+import flask
+from gevent.wsgi import WSGIServer
+import zipfile
+import io
+import base64
+
+
+class ActionRunner:
+    """ActionRunner."""
+    LOG_SENTINEL = 'XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX'
+
+    # initializes the runner
+    # @param source the path where the source code will be located (if any)
+    # @param binary the path where the binary will be located (may be the
+    # same as source code path)
+    def __init__(self, source=None, binary=None):
+        defaultBinary = '/action/exec'
+        self.source = source if source else defaultBinary
+        self.binary = binary if binary else defaultBinary
+
+    def preinit(self):
+        return
+
+    # extracts from the JSON object message a 'code' property and
+    # writes it to the <source> path. The source code may have an
+    # an optional <epilogue>. The source code is subsequently built
+    # to produce the <binary> that is executed during <run>.
+    # @param message is a JSON object, should contain 'code'
+    # @return True iff binary exists and is executable
+    def init(self, message):
+        def prep():
+            self.preinit()
+            if 'code' in message and message['code'] is not None:
+                binary = message['binary'] if 'binary' in message else False
+                if not binary:
+                    return self.initCodeFromString(message)
+                else:
+                    return self.initCodeFromZip(message)
+            else:
+                return False
+
+        if prep():
+            try:
+                # write source epilogue if any
+                # the message is passed along as it may contain other
+                # fields relevant to a specific container.
+                if self.epilogue(message) is False:
+                    return False
+                # build the source
+                if self.build(message) is False:
+                    return False
+            except Exception:
+                return False
+        # verify the binary exists and is executable
+        return self.verify()
+
+    # optionally appends source to the loaded code during <init>
+    def epilogue(self, init_arguments):
+        return
+
+    # optionally builds the source code loaded during <init> into an executable
+    def build(self, init_arguments):
+        return
+
+    # @return True iff binary exists and is executable, False otherwise
+    def verify(self):
+        return (os.path.isfile(self.binary) and
+                os.access(self.binary, os.X_OK))
+
+    # constructs an environment for the action to run in
+    # @param message is a JSON object received from invoker (should
+    # contain 'value' and 'api_key' and other metadata)
+    # @return an environment dictionary for the action process
+    def env(self, message):
+        # make sure to include all the env vars passed in by the invoker
+        env = os.environ
+        for p in ['api_key', 'namespace', 'action_name', 'activation_id', 'deadline']:
+            if p in message:
+                env['__OW_%s' % p.upper()] = message[p]
+        return env
+
+    # runs the action, called iff self.verify() is True.
+    # @param args is a JSON object representing the input to the action
+    # @param env is the environment for the action to run in (defined edge
+    # host, auth key)
+    # return JSON object result of running the action or an error dictionary
+    # if action failed
+    def run(self, args, env):
+        def error(msg):
+            # fall through (exception and else case are handled the same way)
+            sys.stdout.write('%s\n' % msg)
+            return (502, {'error': 'The action did not return a dictionary.'})
+
+        try:
+            input = json.dumps(args)
+            p = subprocess.Popen(
+                [self.binary, input],
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                env=env)
+        except Exception as e:
+            return error(e)
+
+        # run the process and wait until it completes.
+        # stdout/stderr will always be set because we passed PIPEs to Popen
+        (o, e) = p.communicate()
+
+        # stdout/stderr may be either text or bytes, depending on Python
+        # version, so if bytes, decode to text. Note that in Python 2
+        # a string will match both types; so also skip decoding in that case
+        if isinstance(o, bytes) and not isinstance(o, str):
+            o = o.decode('utf-8')
+        if isinstance(e, bytes) and not isinstance(e, str):
+            e = e.decode('utf-8')
+
+        # get the last line of stdout, even if empty
+        lastNewLine = o.rfind('\n', 0, len(o)-1)
+        if lastNewLine != -1:
+            # this is the result string to JSON parse
+            lastLine = o[lastNewLine+1:].strip()
+            # emit the rest as logs to stdout (including last new line)
+            sys.stdout.write(o[:lastNewLine+1])
+        else:
+            # either o is empty or it is the result string
+            lastLine = o.strip()
+
+        if e:
+            sys.stderr.write(e)
+
+        try:
+            json_output = json.loads(lastLine)
+            if isinstance(json_output, dict):
+                return (200, json_output)
+            else:
+                return error(lastLine)
+        except Exception:
+            return error(lastLine)
+
+    # initialize code from inlined string
+    def initCodeFromString(self, message):
+        with codecs.open(self.source, 'w', 'utf-8') as fp:
+            fp.write(message['code'])
+        return True
+
+    # initialize code from base64 encoded archive
+    def initCodeFromZip(self, message):
+        try:
+            bytes = base64.b64decode(message['code'])
+            bytes = io.BytesIO(bytes)
+            archive = zipfile.ZipFile(bytes)
+            archive.extractall(os.path.dirname(self.source))
+            archive.close()
+            return True
+        except Exception as e:
+            print('err', str(e))
+            return False
+
+proxy = flask.Flask(__name__)
+proxy.debug = False
+runner = None
+
+
+def setRunner(r):
+    global runner
+    runner = r
+
+
+@proxy.route('/init', methods=['POST'])
+def init():
+    message = flask.request.get_json(force=True, silent=True)
+    if message and not isinstance(message, dict):
+        flask.abort(404)
+    else:
+        value = message.get('value', {}) if message else {}
+
+    if not isinstance(value, dict):
+        flask.abort(404)
+
+    try:
+        status = runner.init(value)
+    except Exception as e:
+        status = False
+
+    if status is True:
+        return ('OK', 200)
+    else:
+        response = flask.jsonify({'error': 'The action failed to generate or locate a binary. See logs for details.'})
+        response.status_code = 502
+        return complete(response)
+
+
+@proxy.route('/run', methods=['POST'])
+def run():
+    def error():
+        response = flask.jsonify({'error': 'The action did not receive a dictionary as an argument.'})
+        response.status_code = 404
+        return complete(response)
+
+    message = flask.request.get_json(force=True, silent=True)
+    if message and not isinstance(message, dict):
+        return error()
+    else:
+        args = message.get('value', {}) if message else {}
+        if not isinstance(args, dict):
+            return error()
+
+    if runner.verify():
+        try:
+            code, result = runner.run(args, runner.env(message or {}))
+            response = flask.jsonify(result)
+            response.status_code = code
+        except Exception as e:
+            response = flask.jsonify({'error': 'Internal error. {}'.format(e)})
+            response.status_code = 500
+    else:
+        response = flask.jsonify({'error': 'The action failed to locate a binary. See logs for details.'})
+        response.status_code = 502
+    return complete(response)
+
+
+def complete(response):
+    # Add sentinel to stdout/stderr
+    sys.stdout.write('%s\n' % ActionRunner.LOG_SENTINEL)
+    sys.stdout.flush()
+    sys.stderr.write('%s\n' % ActionRunner.LOG_SENTINEL)
+    sys.stderr.flush()
+    return response
+
+
+def main():
+    port = int(os.getenv('FLASK_PROXY_PORT', 8080))
+    server = WSGIServer(('', port), proxy, log=None)
+    server.serve_forever()
+
+if __name__ == '__main__':
+    setRunner(ActionRunner())
+    main()
diff --git a/swift-playground-action/swift3Action/build.gradle b/swift-playground-action/swift3Action/build.gradle
new file mode 100644
index 0000000..0148e31
--- /dev/null
+++ b/swift-playground-action/swift3Action/build.gradle
@@ -0,0 +1,13 @@
+ext.dockerImageName = 'swift3action'
+apply from: '../../gradle/docker.gradle'
+distDocker.dependsOn 'copyProxy'
+distDocker.finalizedBy('rmProxy')
+
+task copyProxy(type: Copy) {
+    from '../actionProxy/actionproxy.py'
+    into './actionproxy.py'
+}
+
+task rmProxy(type: Delete) {
+    delete 'actionproxy.py'
+}
diff --git a/swift-playground-action/swift3Action/buildandrecord.py b/swift-playground-action/swift3Action/buildandrecord.py
new file mode 100644
index 0000000..c83c024
--- /dev/null
+++ b/swift-playground-action/swift3Action/buildandrecord.py
@@ -0,0 +1,76 @@
+"""Python to generate build script.
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+"""
+from __future__ import print_function
+import os
+import sys
+from subprocess import check_output
+
+# Settings
+COMPILE_PREFIX = "/usr/bin/swiftc -module-name Action "
+LINKER_PREFIX = "/usr/bin/swiftc -Xlinker '-rpath=$ORIGIN' " \
+                "'-L/swift3Action/spm-build/.build/release' " \
+                "-o '/swift3Action/spm-build/.build/release/Action'"
+GENERATED_BUILD_SCRIPT = "/swift3Action/spm-build/swiftbuildandlink.sh"
+SPM_DIRECTORY = "/swift3Action/spm-build"
+BUILD_COMMAND = ["swift", "build", "-v", "-c", "release"]
+
+# Build Swift package and capture step trace
+print("Building action")
+out = check_output(BUILD_COMMAND, cwd=SPM_DIRECTORY)
+print("action built. Decoding compile and link commands")
+
+# Look for compile and link commands in step trace
+compileCommand = None
+linkCommand = None
+
+buildInstructions = out.decode("utf-8").splitlines()
+
+for instruction in buildInstructions:
+    if instruction.startswith(COMPILE_PREFIX):
+        compileCommand = instruction
+
+        # add flag to quiet warnings
+        compileCommand += " -suppress-warnings"
+
+    elif instruction.startswith(LINKER_PREFIX):
+        linkCommand = instruction
+
+# Create build script if found, exit otherwise
+if compileCommand is not None and linkCommand is not None:
+    print("Success, command and link commands found.")
+    with open(GENERATED_BUILD_SCRIPT, "a") as buildScript:
+        buildScript.write("#!/bin/bash\n")
+        buildScript.write("echo \"Compiling\"\n")
+        buildScript.write("%s\n" % compileCommand)
+        buildScript.write("swiftStatus=$?\n")
+        buildScript.write("echo swiftc status is $swiftStatus\n")
+        buildScript.write("if [[ \"$swiftStatus\" -eq \"0\" ]]; then\n")
+        buildScript.write("echo \"Linking\"\n")
+        buildScript.write("%s\n" % linkCommand)
+        buildScript.write("else\n")
+        buildScript.write(">&2 echo \"Action did not compile\"\n")
+        buildScript.write("exit 1\n")
+        buildScript.write("fi")
+
+    os.chmod(GENERATED_BUILD_SCRIPT, 0o777)
+    sys.exit(0)
+else:
+    print("Cannot generate build script: compile or link command not found")
+    sys.exit(1)
diff --git a/swift-playground-action/swift3Action/delete-build-run.sh b/swift-playground-action/swift3Action/delete-build-run.sh
new file mode 100755
index 0000000..430c02d
--- /dev/null
+++ b/swift-playground-action/swift3Action/delete-build-run.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+# Useful for local testing.
+# USE WITH CAUTION !!
+
+# This script is useful for testing the action proxy (or its derivatives)
+# in combination with [init,run].py. Use it to rebuild the container image
+# and start the proxy: delete-build-run.sh whisk/dockerskeleton.
+
+# Removes all previously built instances.
+remove=$(docker ps -a -q)
+if [[ !  -z  $remove  ]]; then
+    docker rm $remove
+fi
+
+image=${1:-openwhisk/dockerskeleton}
+docker build -t $image .
+
+echo ""
+echo "  ---- RUNNING ---- "
+echo ""
+
+docker run -i -t -p 8080:8080 $image
diff --git a/swift-playground-action/swift3Action/epilogue.swift b/swift-playground-action/swift3Action/epilogue.swift
new file mode 100644
index 0000000..9107234
--- /dev/null
+++ b/swift-playground-action/swift3Action/epilogue.swift
@@ -0,0 +1,78 @@
+
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+// Deliberate whitespaces above.
+
+/* This code is appended to user-supplied action code.
+ It reads from the standard input, deserializes into JSON and invokes the
+ main function. Currently, actions print strings to stdout. This can evolve once
+ JSON serialization is available in Foundation. */
+
+import Foundation
+
+#if os(Linux)
+    import Glibc
+#endif
+
+func _whisk_json2dict(txt: String) -> [String:Any]? {
+    if let data = txt.data(using: String.Encoding.utf8, allowLossyConversion: true) {
+        do {
+            return WhiskJsonUtils.jsonDataToDictionary(jsonData: data)
+        } catch {
+            return nil
+        }
+    }
+    return nil
+}
+
+
+func _run_main(mainFunction: ([String: Any]) -> [String: Any]) -> Void {
+    let env = ProcessInfo.processInfo.environment
+    let inputStr: String = env["WHISK_INPUT"] ?? "{}"
+    
+    if let parsed = _whisk_json2dict(txt: inputStr) {
+        let result = mainFunction(parsed)
+        
+        if result is [String:Any] {
+            do {
+                if let respString = WhiskJsonUtils.dictionaryToJsonString(jsonDict: result) {
+                    print("\(respString)")
+                } else {
+                    print("Error converting \(result) to JSON string")
+                    #if os(Linux)
+                        fputs("Error converting \(result) to JSON string", stderr)
+                    #endif
+                }
+            } catch {
+                print("Error serializing response \(error)")
+                #if os(Linux)
+                    fputs("Error serializing response \(error)", stderr)
+                #endif
+            }
+        } else {
+            print("Cannot serialize response: \(result)")
+            #if os(Linux)
+                fputs("Cannot serialize response: \(result)", stderr)
+            #endif
+        }
+    } else {
+        print("Error: couldn't parse JSON input.")
+        #if os(Linux)
+            fputs("Error: couldn't parse JSON input.", stderr)
+        #endif
+    }
+}
diff --git a/swift-playground-action/swift3Action/invoke.py b/swift-playground-action/swift3Action/invoke.py
new file mode 100755
index 0000000..92d8a6d
--- /dev/null
+++ b/swift-playground-action/swift3Action/invoke.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+"""Executable Python script for testing the action proxy.
+
+  This script is useful for testing the action proxy (or its derivatives)
+  by simulating invoker interactions. Use it in combination with
+  delete-build-run.sh which builds and starts up the action proxy.
+  Examples:
+     ./delete-build-run.sh &
+     ./invoke.py init <action source file> # should return OK
+     ./invoke.py run '{"some":"json object as a string"}'
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+"""
+
+import os
+import re
+import sys
+import json
+import requests
+import codecs
+
+DOCKER_HOST = "localhost"
+if "DOCKER_HOST" in os.environ:
+    try:
+        DOCKER_HOST = re.compile("tcp://(.*):[\d]+").findall(
+            os.environ["DOCKER_HOST"])[0]
+    except Exception:
+        print("cannot determine docker host from %s" %
+              os.environ["DOCKER_HOST"])
+        sys.exit(-1)
+DEST = "http://%s:8080" % DOCKER_HOST
+
+
+def content_from_args(args):
+    if len(args) == 0:
+        return None
+
+    if len(args) == 1 and os.path.exists(args[0]):
+        with open(args[0]) as fp:
+            return json.load(fp)
+
+    # else...
+    in_str = " ".join(args)
+    try:
+        d = json.loads(in_str)
+        if isinstance(d, dict):
+            return d
+        else:
+            raise "Not a dict."
+    except:
+        return in_str
+
+
+def init(args):
+    main = args[1] if len(args) == 2 else "main"
+    args = args[0] if len(args) >= 1 else None
+
+    if args and args.endswith(".zip"):
+        with open(args, "rb") as fp:
+            contents = fp.read().encode("base64")
+        binary = True
+    elif args:
+        with(codecs.open(args, "r", "utf-8")) as fp:
+            contents = fp.read()
+        binary = False
+    else:
+        contents = None
+        binary = False
+
+    r = requests.post("%s/init" % DEST, json={"value": {"code": contents,
+                                                        "binary": binary,
+                                                        "main": main}})
+    print(r.text)
+
+
+def run(args):
+    value = content_from_args(args)
+    # print("Sending value: %s..." % json.dumps(value)[0:40])
+    r = requests.post("%s/run" % DEST, json={"value": value})
+    print(r.text)
+
+
+if sys.argv[1] == "init":
+    init(sys.argv[2:])
+elif sys.argv[1] == "run":
+    run(sys.argv[2:])
+else:
+    print("usage: 'init <filename>' or 'run JSON-as-string'")
diff --git a/swift-playground-action/swift3Action/spm-build/Package.swift b/swift-playground-action/swift3Action/spm-build/Package.swift
new file mode 100644
index 0000000..c4b964d
--- /dev/null
+++ b/swift-playground-action/swift3Action/spm-build/Package.swift
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+        dependencies: [
+    .Package(url: "https://github.com/IBM-Swift/Kitura-net.git", "1.0.1"),
+            .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", "14.2.0"),
+            .Package(url: "https://github.com/IBM-Swift/swift-watson-sdk.git", "0.4.1")
+        ]
+)
diff --git a/swift-playground-action/swift3Action/spm-build/_Whisk.swift b/swift-playground-action/swift3Action/spm-build/_Whisk.swift
new file mode 100644
index 0000000..d1124ed
--- /dev/null
+++ b/swift-playground-action/swift3Action/spm-build/_Whisk.swift
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+import Foundation
+import KituraNet
+import Dispatch
+
+class Whisk {
+    class func invoke(actionNamed action : String, withParameters params : [String:Any], blocking: Bool = true) -> [String:Any] {
+        let parsedAction = parseQualifiedName(name: action)
+        let strBlocking = blocking ? "true" : "false"
+        let path = "/api/v1/namespaces/\(parsedAction.namespace)/actions/\(parsedAction.name)?blocking=\(strBlocking)"
+
+        return postSyncronish(uriPath: path, params: params)
+    }
+
+    class func trigger(eventNamed event : String, withParameters params : [String:Any]) -> [String:Any] {
+        let parsedEvent = parseQualifiedName(name: event)
+        let path = "/api/v1/namespaces/\(parsedEvent.namespace)/triggers/\(parsedEvent.name)?blocking=true"
+
+        return postSyncronish(uriPath: path, params: params)
+    }
+
+    // handle the GCD dance to make the post async, but then obtain/return
+    // the result from this function sync
+    private class func postSyncronish(uriPath path: String, params : [String:Any]) -> [String:Any] {
+        var response : [String:Any]!
+
+        let queue = DispatchQueue.global()
+        let invokeGroup = DispatchGroup()
+
+        invokeGroup.enter()
+        queue.async {
+            post(uriPath: path, params: params, group: invokeGroup) { result in
+                response = result
+            }
+        }
+
+        // On one hand, FOREVER seems like an awfully long time...
+        // But on the other hand, I think we can rely on the system to kill this
+        // if it exceeds a reasonable execution time.
+        switch invokeGroup.wait(timeout: DispatchTime.distantFuture) {
+        case DispatchTimeoutResult.success:
+            break
+        case DispatchTimeoutResult.timedOut:
+            break
+        }
+
+        return response
+    }
+
+    /**
+     * Initializes with host, port and authKey determined from environment variables
+     * __OW_API_HOST and __OW_API_KEY, respectively.
+     */
+    private class func initializeCommunication() -> (httpType: String, host : String, port : Int16, authKey : String) {
+        let env = ProcessInfo.processInfo.environment
+
+        var edgeHost : String!
+        if let edgeHostEnv : String = env["__OW_API_HOST"] {
+            edgeHost = "\(edgeHostEnv)"
+        } else {
+            fatalError("__OW_API_HOST environment variable was not set.")
+        }
+
+        var protocolIndex = edgeHost.startIndex
+
+        //default to https
+        var httpType = "https://"
+        var port : Int16 = 443
+
+        // check if protocol is included in environment variable
+        if edgeHost.hasPrefix("https://") {
+            protocolIndex = edgeHost.index(edgeHost.startIndex, offsetBy: 8)
+        } else if edgeHost.hasPrefix("http://") {
+            protocolIndex = edgeHost.index(edgeHost.startIndex, offsetBy: 7)
+            httpType = "http://"
+            port = 80
+        }
+
+        let hostname = edgeHost.substring(from: protocolIndex)
+        let hostComponents = hostname.components(separatedBy: ":")
+
+        let host = hostComponents[0]
+
+        if hostComponents.count == 2 {
+            port = Int16(hostComponents[1])!
+        }
+
+        var authKey = "authKey"
+        if let authKeyEnv : String = env["__OW_API_KEY"] {
+            authKey = authKeyEnv
+        }
+
+        return (httpType, host, port, authKey)
+    }
+
+    // actually do the POST call to the specified OpenWhisk URI path
+    private class func post(uriPath: String, params : [String:Any], group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
+        let communicationDetails = initializeCommunication()
+
+        let loginData: Data = communicationDetails.authKey.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+        let base64EncodedAuthKey  = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
+
+        let headers = ["Content-Type" : "application/json",
+                       "Authorization" : "Basic \(base64EncodedAuthKey)"]
+
+        guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
+            callback(["error": "Error encoding uri path to make openwhisk REST call."])
+            return
+        }
+
+        // TODO vary the schema based on the port?
+        let requestOptions = [ClientRequest.Options.schema(communicationDetails.httpType),
+                              ClientRequest.Options.method("post"),
+                              ClientRequest.Options.hostname(communicationDetails.host),
+                              ClientRequest.Options.port(communicationDetails.port),
+                              ClientRequest.Options.path(encodedPath),
+                              ClientRequest.Options.headers(headers),
+                              ClientRequest.Options.disableSSLVerification]
+
+        let request = HTTP.request(requestOptions) { response in
+
+            // exit group after we are done
+            defer {
+                group.leave()
+            }
+
+            if response != nil {
+                do {
+                    // this is odd, but that's just how KituraNet has you get
+                    // the response as NSData
+                    var jsonData = Data()
+                    try response!.readAllData(into: &jsonData)
+
+                    switch WhiskJsonUtils.getJsonType(jsonData: jsonData) {
+                    case .Dictionary:
+                        if let resp = WhiskJsonUtils.jsonDataToDictionary(jsonData: jsonData) {
+                            callback(resp)
+                        } else {
+                            callback(["error": "Could not parse a valid JSON response."])
+                        }
+                    case .Array:
+                        if WhiskJsonUtils.jsonDataToArray(jsonData: jsonData) != nil {
+                            callback(["error": "Response is an array, expecting dictionary."])
+                        } else {
+                            callback(["error": "Could not parse a valid JSON response."])
+                        }
+                    case .Undefined:
+                        callback(["error": "Could not parse a valid JSON response."])
+                    }
+
+                } catch {
+                    callback(["error": "Could not parse a valid JSON response."])
+                }
+            } else {
+                callback(["error": "Did not receive a response."])
+            }
+        }
+
+        // turn params into JSON data
+        if let jsonData = WhiskJsonUtils.dictionaryToJsonString(jsonDict: params) {
+            request.write(from: jsonData)
+            request.end()
+        } else {
+            callback(["error": "Could not parse parameters."])
+            group.leave()
+        }
+
+    }
+
+    /**
+     * This function is currently unused but ready when we want to switch to using URLSession instead of KituraNet.
+     */
+    private class func postUrlSession(uriPath: String, params : [String:Any], group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
+        let communicationDetails = initializeCommunication()
+
+        guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
+            callback(["error": "Error encoding uri path to make openwhisk REST call."])
+            return
+        }
+
+        let urlStr = "\(communicationDetails.httpType)\(communicationDetails.host):\(communicationDetails.port)\(encodedPath)"
+
+        if let url = URL(string: urlStr) {
+            var request = URLRequest(url: url)
+            request.httpMethod = "POST"
+
+            do {
+                request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+                request.httpBody = try JSONSerialization.data(withJSONObject: params)
+
+                let loginData: Data = communicationDetails.authKey.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+                let base64EncodedAuthKey  = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
+                request.addValue("Basic \(base64EncodedAuthKey)", forHTTPHeaderField: "Authorization")
+
+                let session = URLSession(configuration: URLSessionConfiguration.default)
+
+                let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
+
+                    // exit group after we are done
+                    defer {
+                        group.leave()
+                    }
+
+                    if let error = error {
+                        callback(["error":error.localizedDescription])
+                    } else {
+
+                        if let data = data {
+                            do {
+                                let respJson = try JSONSerialization.jsonObject(with: data)
+                                if respJson is [String:Any] {
+                                    callback(respJson as! [String:Any])
+                                } else {
+                                    callback(["error":" response from server is not a dictionary"])
+                                }
+                            } catch {
+                                callback(["error":"Error creating json from response: \(error)"])
+                            }
+                        }
+                    }
+                })
+
+                task.resume()
+            } catch {
+                callback(["error":"Got error creating params body: \(error)"])
+            }
+        }
+    }
+
+    // separate an OpenWhisk qualified name (e.g. "/whisk.system/samples/date")
+    // into namespace and name components
+    private class func parseQualifiedName(name qualifiedName : String) -> (namespace : String, name : String) {
+        let defaultNamespace = "_"
+        let delimiter = "/"
+
+        let segments :[String] = qualifiedName.components(separatedBy: delimiter)
+
+        if segments.count > 2 {
+            return (segments[1], Array(segments[2..<segments.count]).joined(separator: delimiter))
+        } else {
+            // allow both "/theName" and "theName"
+            let name = qualifiedName.hasPrefix(delimiter) ? segments[1] : segments[0]
+            return (defaultNamespace, name)
+        }
+    }
+}
diff --git a/swift-playground-action/swift3Action/spm-build/_WhiskJSONUtils.swift b/swift-playground-action/swift3Action/spm-build/_WhiskJSONUtils.swift
new file mode 100644
index 0000000..f9e6460
--- /dev/null
+++ b/swift-playground-action/swift3Action/spm-build/_WhiskJSONUtils.swift
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+import Foundation
+import SwiftyJSON
+
+enum WhiskJsonType {
+    case Dictionary
+    case Array
+    case Undefined
+}
+
+class WhiskJsonUtils {
+
+    class func getJsonType(jsonData: Data) -> WhiskJsonType {
+        do {
+            let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
+            if json is [String:Any] {
+                return .Dictionary
+            } else if json is [Any] {
+                return .Array
+            } else {
+                return .Undefined
+            }
+        } catch {
+            print("Error converting JSON data to dictionary \(error)")
+            return .Undefined
+        }
+    }
+
+    class func jsonDataToArray(jsonData: Data) -> [Any]? {
+        do {
+            let arr = try JSONSerialization.jsonObject(with: jsonData, options: [])
+            return (arr as? [Any])
+        } catch {
+            print("Error converting JSON data to dictionary \(error)")
+            return nil
+        }
+    }
+
+    class func jsonDataToDictionary(jsonData: Data) -> [String:Any]? {
+        do {
+            let dic = try JSONSerialization.jsonObject(with: jsonData, options: [])
+            return dic as? [String:Any]
+        } catch {
+            print("Error converting JSON data to dictionary \(error)")
+            return nil
+        }
+    }
+
+    // use SwiftyJSON to serialize JSON object because of bug in Linux Swift 3.0
+    // https://github.com/IBM-Swift/SwiftRuntime/issues/230
+    class func dictionaryToJsonString(jsonDict: [String:Any]) -> String? {
+        if JSONSerialization.isValidJSONObject(jsonDict) {
+            do {
+                let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
+                if let jsonStr = String(data: jsonData, encoding: String.Encoding.utf8) {
+                    return jsonStr
+                } else {
+                    print("Error serializing data to JSON, data conversion returns nil string")
+                }
+            } catch {
+                print(("\(error)"))
+            }
+        } else {
+            print("Error serializing JSON, data does not appear to be valid JSON")
+        }
+        return nil
+    }
+
+    class func dictionaryToData(jsonDict: [String:Any]) -> Data? {
+        let json: JSON = JSON(jsonDict)
+
+        do {
+            let data: Data = try json.rawData()
+            return data
+        } catch {
+            print("Cannot convert Dictionary to Data")
+            return nil
+        }
+    }
+
+    class func arrayToJsonString(jsonArray: [Any]) -> String? {
+        let json: JSON = JSON(jsonArray)
+
+        if let jsonStr = json.rawString() {
+            let trimmed = jsonStr.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\r", with: "")
+            return trimmed
+        } else {
+            return nil
+        }
+    }
+
+    class func arrayToData(jsonArray: [Any]) -> Data? {
+        let json: JSON = JSON(jsonArray)
+
+        do {
+            let data: Data = try json.rawData()
+            return data
+        } catch {
+            print("Cannot convert Array to Data")
+            return nil
+        }
+    }
+
+    private class func escapeDict(json: [String:Any]) -> [String:Any] {
+        var escaped = [String:Any]()
+
+        for (k,v) in json {
+            if v is String {
+                let str = (v as! String).replacingOccurrences(of:"\"", with:"\\\"")
+                escaped[k] = str
+            } else if v is [String:Any] {
+                escaped[k] = escapeDict(json: v as! [String : Any])
+            } else if v is [Any] {
+                escaped[k] = escapeArray(json: v as! [Any])
+            } else {
+                escaped[k] = v
+            }
+        }
+        return escaped
+    }
+
+    private class func escapeArray(json: [Any]) -> [Any] {
+        var escaped = [Any]()
+
+        for v in json {
+            if v is String {
+                let str = (v as! String).replacingOccurrences(of:"\"", with:"\\\"")
+                escaped.append(str)
+            } else if v is [String:Any] {
+                let dic = escapeDict(json: v as! [String:Any])
+                escaped.append(dic)
+            } else if v is [Any] {
+                let arr = escapeArray(json: v as! [Any])
+                escaped.append(arr)
+            } else {
+                escaped.append(v)
+            }
+        }
+
+        return escaped
+    }
+
+    private class func escape(json: Any) -> Any? {
+        if json is [String:Any] {
+            let escapeObj = json as! [String:Any]
+            return escapeDict(json: escapeObj)
+        } else if json is [Any] {
+            let escapeObj = json as! [Any]
+            return escapeArray(json: escapeObj)
+        } else {
+            return nil
+        }
+    }
+}
diff --git a/swift-playground-action/swift3Action/stub.sh b/swift-playground-action/swift3Action/stub.sh
new file mode 100644
index 0000000..842d00a
--- /dev/null
+++ b/swift-playground-action/swift3Action/stub.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+echo \
+'This is a stub action that should be replaced with user code (e.g., script or compatible binary).
+The input to the action is received as an argument from the command line.
+Actions may log to stdout or stderr. By convention, the last line of output must
+be a stringified JSON object which represents the result of the action.'
+
+echo '{ "error": "This is a stub action. Replace it with custom logic." }'
\ No newline at end of file
diff --git a/swift-playground-action/swift3Action/swift3runner.py b/swift-playground-action/swift3Action/swift3runner.py
new file mode 100644
index 0000000..f41456d
--- /dev/null
+++ b/swift-playground-action/swift3Action/swift3runner.py
@@ -0,0 +1,114 @@
+"""Python proxy to run Swift action.
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+"""
+import os
+import glob
+import sys
+import subprocess
+import codecs
+import json
+sys.path.append('../actionProxy')
+from actionproxy import ActionRunner, main, setRunner  # noqa
+
+SRC_EPILOGUE_FILE = '/swift3Action/epilogue.swift'
+DEST_SCRIPT_FILE = '/swift3Action/spm-build/main.swift'
+DEST_SCRIPT_DIR = '/swift3Action/spm-build'
+DEST_BIN_FILE = '/swift3Action/spm-build/.build/release/Action'
+
+BUILD_PROCESS = ['./swiftbuildandlink.sh']
+
+
+class Swift3Runner(ActionRunner):
+
+    def __init__(self):
+        ActionRunner.__init__(self, DEST_SCRIPT_FILE, DEST_BIN_FILE)
+
+    # remove pre-existing binary before receiving a new binary
+    def preinit(self):
+        try:
+            os.remove(self.binary)
+        except: pass
+
+    def epilogue(self, init_message):
+        # skip if executable already exists (was unzipped)
+        if os.path.isfile(self.binary):
+            return
+
+        if 'main' in init_message:
+            main_function = init_message['main']
+        else:
+            main_function = 'main'
+        # make sure there is a main.swift file
+        open(DEST_SCRIPT_FILE, 'a').close()
+
+        with codecs.open(DEST_SCRIPT_FILE, 'a', 'utf-8') as fp:
+            os.chdir(DEST_SCRIPT_DIR)
+            for file in glob.glob("*.swift"):
+                if file not in ["Package.swift", "main.swift", "_WhiskJSONUtils.swift", "_Whisk.swift"]:
+                    with codecs.open(file, 'r', 'utf-8') as f:
+                        fp.write(f.read())
+            with codecs.open(SRC_EPILOGUE_FILE, 'r', 'utf-8') as ep:
+                fp.write(ep.read())
+
+            fp.write('_run_main(mainFunction: %s)\n' % main_function)
+
+    def build(self, init_message):
+        # short circuit the build, if there already exists a binary
+        # from the zip file
+        if os.path.isfile(self.binary):
+            # file may not have executable permission, set it
+            os.chmod(self.binary, 0o555)
+            return
+
+        p = subprocess.Popen(
+            BUILD_PROCESS,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            cwd=DEST_SCRIPT_DIR)
+
+        # run the process and wait until it completes.
+        # stdout/stderr will not be None because we passed PIPEs to Popen
+        (o, e) = p.communicate()
+
+        # stdout/stderr may be either text or bytes, depending on Python
+        # version, so if bytes, decode to text. Note that in Python 2
+        # a string will match both types; so also skip decoding in that case
+        if isinstance(o, bytes) and not isinstance(o, str):
+            o = o.decode('utf-8')
+        if isinstance(e, bytes) and not isinstance(e, str):
+            e = e.decode('utf-8')
+
+        if o:
+            sys.stdout.write(o)
+            sys.stdout.flush()
+
+        if e:
+            sys.stderr.write(e)
+            sys.stderr.flush()
+
+    def env(self, message):
+        env = ActionRunner.env(self, message)
+        args = message.get('value', {}) if message else {}
+        env['WHISK_INPUT'] = json.dumps(args)
+        return env
+
+
+if __name__ == '__main__':
+    setRunner(Swift3Runner())
+    main()
diff --git a/swift-playground-action/test0.swift b/swift-playground-action/test0.swift
new file mode 100644
index 0000000..1c6ee55
--- /dev/null
+++ b/swift-playground-action/test0.swift
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+import Foundation
+
+print("Hello World");
diff --git a/swift-playground-action/test1.swift b/swift-playground-action/test1.swift
new file mode 100644
index 0000000..8dc17c7
--- /dev/null
+++ b/swift-playground-action/test1.swift
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+
+//let json: JSON = "I'm a json"
+let json = JSON(["name":"Joe Doe", "age": 25])
+if let name = json["name"].string {
+   print("Hello", name);
+}
diff --git a/swift-playground-action/test2.swift b/swift-playground-action/test2.swift
new file mode 100644
index 0000000..15df760
--- /dev/null
+++ b/swift-playground-action/test2.swift
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+import KituraNet
+import Dispatch
+import SSLService
+
+//let json: JSON = "I'm a json"
+// let json = JSON(["name":"Paul", "age": 25])
+// if let name = json["name"].string {
+//    print("Hello ", name);
+// }
diff --git a/swift-playground-action/test3.swift b/swift-playground-action/test3.swift
new file mode 100644
index 0000000..97c0e1e
--- /dev/null
+++ b/swift-playground-action/test3.swift
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+import Conversation
+import RestKit
+
+//let json: JSON = "I'm a json"
+// let json = JSON(["name":"Paul", "age": 25])
+// if let name = json["name"].string {
+//    print("Hello ", name);
+// }
+
+let username = "aa50bee6-a71b-47d3-8dca-e23b628315e3"
+let password = "mtqVUlA5WCZy"
+let version = "2016-07-19" // use today's date for the most recent version
+let conversation = Conversation(username: username, password: password, version: version)
+
+let workspaceID = "b4ff2246-c42f-407e-b172-72a631cf5498"
+let failure = { (error: RestError) in print("error", error) }
+var context: Context? // save context to continue conversation
+var text = "hello"
+conversation.message(workspaceID: workspaceID, text: text, failure: failure) { response in
+    //print(response.output.text)
+    print("response",response)
+    context = response.context
+}
+
+// conversation.message(workspaceID: workspaceID, text: text, context: context, failure: failure) { response in
+//     print(response.output.text)
+//     context = response.context
+// }
diff --git a/swift-playground-action/test4.swift b/swift-playground-action/test4.swift
new file mode 100644
index 0000000..ea3ff8a
--- /dev/null
+++ b/swift-playground-action/test4.swift
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+import Conversation
+import RestKit
+
+//var serviceURL = "https://jsonplaceholder.typicode.com"
+
+let username = "aa50bee6-a71b-47d3-8dca-e23b628315e3"
+let password = "mtqVUlA5WCZy"
+let version = "2016-07-19" 
+
+var serviceURL = "https://gateway.watsonplatform.net/conversation/api"
+
+var queryParameters = [URLQueryItem]()
+queryParameters.append(URLQueryItem(name: "version", value: version))
+
+let request = RestRequest(
+        method: .GET,
+        url: serviceURL + "/v1/workspaces",
+        acceptType: "application/json",
+        contentType: "application/json",
+        queryParameters: queryParameters,
+        username: username,
+        password: password         
+    )
+
+let failure = { (error: RestError) in print("error", error) }
+
+let success = { (msg) in print("success", msg) }
+
+// execute REST request
+request.responseJSON { response in
+    switch response {
+    case .success(let json): success(json)
+    case .failure(let error): failure(error)
+    }
+}