├── .gitignore ├── .travis.yml ├── README.md ├── activator ├── activator-launch-1.3.5.jar ├── activator.bat ├── app ├── Boot.scala ├── models │ └── Model.scala ├── repository │ ├── InMemoryData.scala │ └── UsersRepository.scala ├── routers │ ├── OrdersRouter.scala │ ├── ProductsRouter.scala │ └── UsersRouter.scala └── views │ ├── index.scala.html │ └── main.scala.html ├── build.sbt ├── conf ├── application.conf └── logback.xml ├── project ├── build.properties └── plugins.sbt └── test └── routers ├── BaseRouterSpecification.scala ├── OrdersRouterSpec.scala ├── ProductsRouterSpec.scala └── UsersRouterSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 2.11.7 3 | jdk: oraclejdk8 4 | script: "sbt clean coverage test" 5 | after_success: "sbt coveralls" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | light-play-rest-api 2 | =================== 3 | 4 | [![Codeship Status for gvolpe/light-play-rest-api](https://codeship.com/projects/d01bfc40-1767-0133-8ea3-06c83ac03245/status?branch=master)](https://codeship.com/projects/93580) 5 | [![Coverage Status](https://coveralls.io/repos/gvolpe/light-play-rest-api/badge.svg?branch=master&service=github)](https://coveralls.io/github/gvolpe/light-play-rest-api?branch=master) 6 | [![Codacy Badge](https://www.codacy.com/project/badge/2a966c471ffd466ca91bd175d292c9d9)](https://www.codacy.com/app/volpegabriel/light-play-rest-api) 7 | 8 | This project aims to be the reference to create a Light Weight REST API using [Play Framework 2.4.x](https://www.playframework.com/). 9 | 10 | ## About 11 | 12 | We are using customized routers, easy to re-use them and fully testables. Every router represents a funcionality (separation of concerns). No more single 'Routes' file that becomes huge with the time. 13 | 14 | ## How it works? 15 | 16 | The entry point is a custom class that extends [ApplicationLoader](https://www.playframework.com/documentation/tr/2.4.x/api/scala/index.html#play.api.ApplicationLoader) which defines the 'load' method. 17 | 18 | ```scala 19 | class Boot extends ApplicationLoader { 20 | 21 | def load(context: Context): Application = ??? 22 | 23 | } 24 | ``` 25 | To indicate that we want to use this custom loader, we have to specify it in the **application.conf** file. 26 | 27 | ``` 28 | play.application.loader = Boot 29 | ``` 30 | 31 | After that we have to create the customized routers. This project has 3 sample routers for Users, Products and Orders. This is how it looks: 32 | 33 | ```scala 34 | object UsersRouter extends DefaultUsersRepository with UsersRouter { 35 | def apply(): Router.Routes = routes 36 | } 37 | 38 | trait UsersRouter { 39 | 40 | self: UsersRepository => 41 | 42 | def routes: Router.Routes = { 43 | 44 | case GET(p"/users/${long(id)}") => Action.async { 45 | find(id) map { 46 | case Some(user) => Ok(Json.toJson(user)) 47 | case None => NotFound 48 | } 49 | } 50 | 51 | case POST(p"/users") => Action.async(parse.json[User]) { implicit request => 52 | val user = request.body 53 | save(user) map (_ => Created) 54 | } 55 | 56 | } 57 | 58 | } 59 | ``` 60 | 61 | Since the type [Router.Routes](https://www.playframework.com/documentation/tr/2.4.x/api/scala/index.html#play.api.routing.Router$@Routes=PartialFunction[play.api.mvc.RequestHeader,play.api.mvc.Handler]) is a **PartialFunction[RequestHeader, Handler]**, we can define the cases in our 'routes' method. We created a companion object with an apply() method to make things easier. 62 | 63 | ### Putting the pieces together 64 | 65 | After all, we are able to combine our Routes in a defined Router as shown in the code below: 66 | 67 | ```scala 68 | def router: Router = Router.from { 69 | UsersRouter() orElse 70 | ProductsRouter() orElse 71 | OrdersRouter() 72 | } 73 | ``` 74 | 75 | There are different ways to combine PartialFunctions to get only one. We choose the first one but we can, for instance, define the Router by reducing a List of PartialFunctions, as we do in the [BaseRouterSpecification](https://github.com/gvolpe/light-play-rest-api/blob/master/test/routers/BaseRouterSpecification.scala) class: 76 | ```scala 77 | def router: Router = Router.from { 78 | val routers = List(UsersRouter(), ProductsRouter(), OrdersRouter()) 79 | routers reduceLeft (_ orElse _) 80 | } 81 | ``` 82 | 83 | You can find more information about the new Routing system in the [Official Documentation](https://www.playframework.com/documentation/2.4.x/ScalaSirdRouter). 84 | 85 | That's enough to start a Light REST API from the scratch ***thinking seriously in clean coding***. 86 | 87 | ## Testing 88 | 89 | We choose **Specs2** to test Play's applications. As it's explained in the documentation, we are able to [test the Routers](https://www.playframework.com/documentation/2.4.x/ScalaFunctionalTestingWithSpecs2#Testing-the-router) instead of call directly to an Action. 90 | 91 | So we start creating a Specification for the UsersRouterSpec as shown below: 92 | 93 | ```scala 94 | class UsersRouterSpec extends PlaySpecification { 95 | ...... 96 | } 97 | ``` 98 | 99 | Then we create a FakeUsersRouter to replace the Real Users Repository for an InMemoryRepository (Default). In the application we are using the same repository, but that will be changed for a repository accessing a real database. 100 | 101 | ```scala 102 | object FakeUsersRouter extends DefaultUsersRepository with UsersRouter { 103 | def apply(): Router.Routes = routes 104 | } 105 | ``` 106 | 107 | Finally we create a fakeApplicationLoader to start testing the Routers. 108 | 109 | ```scala 110 | val fakeRouter = Router.from(FakeUsersRouter()) 111 | 112 | val fakeAppLoader = new ApplicationLoader() { 113 | def load(context: Context): Application = new BuiltInComponentsFromContext(context) { 114 | def router = fakeRouter 115 | }.application 116 | } 117 | ``` 118 | 119 | Now we are able to write our tests, using our Fake Application Loader. 120 | 121 | ```scala 122 | "Users Router" should { 123 | 124 | "Not find the user" in new WithApplicationLoader(fakeAppLoader) { 125 | val fakeRequest = FakeRequest(GET, "/users/123") 126 | val Some(result) = route(fakeRequest) 127 | 128 | status(result) must equalTo(NOT_FOUND) 129 | } 130 | 131 | ... More cases ... 132 | } 133 | ``` 134 | 135 | To learn more see the [completed test specification](https://github.com/gvolpe/light-play-rest-api/blob/master/test/routers/UsersRouterSpec.scala). 136 | 137 | ## License 138 | 139 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 140 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 141 | 142 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 143 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 144 | language governing permissions and limitations under the License. 145 | -------------------------------------------------------------------------------- /activator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ### ------------------------------- ### 4 | ### Helper methods for BASH scripts ### 5 | ### ------------------------------- ### 6 | 7 | realpath () { 8 | ( 9 | TARGET_FILE="$1" 10 | 11 | cd "$(dirname "$TARGET_FILE")" 12 | TARGET_FILE=$(basename "$TARGET_FILE") 13 | 14 | COUNT=0 15 | while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] 16 | do 17 | TARGET_FILE=$(readlink "$TARGET_FILE") 18 | cd "$(dirname "$TARGET_FILE")" 19 | TARGET_FILE=$(basename "$TARGET_FILE") 20 | COUNT=$(($COUNT + 1)) 21 | done 22 | 23 | if [ "$TARGET_FILE" == "." -o "$TARGET_FILE" == ".." ]; then 24 | cd "$TARGET_FILE" 25 | TARGET_FILEPATH= 26 | else 27 | TARGET_FILEPATH=/$TARGET_FILE 28 | fi 29 | 30 | # make sure we grab the actual windows path, instead of cygwin's path. 31 | if ! is_cygwin; then 32 | echo "$(pwd -P)/$TARGET_FILE" 33 | else 34 | echo $(cygwinpath "$(pwd -P)/$TARGET_FILE") 35 | fi 36 | ) 37 | } 38 | 39 | # TODO - Do we need to detect msys? 40 | 41 | # Uses uname to detect if we're in the odd cygwin environment. 42 | is_cygwin() { 43 | local os=$(uname -s) 44 | case "$os" in 45 | CYGWIN*) return 0 ;; 46 | *) return 1 ;; 47 | esac 48 | } 49 | 50 | # This can fix cygwin style /cygdrive paths so we get the 51 | # windows style paths. 52 | cygwinpath() { 53 | local file="$1" 54 | if is_cygwin; then 55 | echo $(cygpath -w $file) 56 | else 57 | echo $file 58 | fi 59 | } 60 | 61 | # Make something URI friendly 62 | make_url() { 63 | url="$1" 64 | local nospaces=${url// /%20} 65 | if is_cygwin; then 66 | echo "/${nospaces//\\//}" 67 | else 68 | echo "$nospaces" 69 | fi 70 | } 71 | 72 | # Detect if we should use JAVA_HOME or just try PATH. 73 | get_java_cmd() { 74 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 75 | echo "$JAVA_HOME/bin/java" 76 | else 77 | echo "java" 78 | fi 79 | } 80 | 81 | echoerr () { 82 | echo 1>&2 "$@" 83 | } 84 | vlog () { 85 | [[ $verbose || $debug ]] && echoerr "$@" 86 | } 87 | dlog () { 88 | [[ $debug ]] && echoerr "$@" 89 | } 90 | execRunner () { 91 | # print the arguments one to a line, quoting any containing spaces 92 | [[ $verbose || $debug ]] && echo "# Executing command line:" && { 93 | for arg; do 94 | if printf "%s\n" "$arg" | grep -q ' '; then 95 | printf "\"%s\"\n" "$arg" 96 | else 97 | printf "%s\n" "$arg" 98 | fi 99 | done 100 | echo "" 101 | } 102 | 103 | exec "$@" 104 | } 105 | addJava () { 106 | dlog "[addJava] arg = '$1'" 107 | java_args=( "${java_args[@]}" "$1" ) 108 | } 109 | addApp () { 110 | dlog "[addApp] arg = '$1'" 111 | sbt_commands=( "${app_commands[@]}" "$1" ) 112 | } 113 | addResidual () { 114 | dlog "[residual] arg = '$1'" 115 | residual_args=( "${residual_args[@]}" "$1" ) 116 | } 117 | addDebugger () { 118 | addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" 119 | } 120 | addConfigOpts () { 121 | dlog "[addConfigOpts] arg = '$*'" 122 | for item in $* 123 | do 124 | addJava "$item" 125 | done 126 | } 127 | # a ham-fisted attempt to move some memory settings in concert 128 | # so they need not be messed around with individually. 129 | get_mem_opts () { 130 | local mem=${1:-1024} 131 | local meta=$(( $mem / 4 )) 132 | (( $meta > 256 )) || meta=256 133 | (( $meta < 1024 )) || meta=1024 134 | 135 | # default is to set memory options but this can be overridden by code section below 136 | memopts="-Xms${mem}m -Xmx${mem}m" 137 | if [[ "${java_version}" > "1.8" ]]; then 138 | extmemopts="-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=${meta}m" 139 | else 140 | extmemopts="-XX:PermSize=64m -XX:MaxPermSize=${meta}m" 141 | fi 142 | 143 | if [[ "${java_opts}" == *-Xmx* ]] || [[ "${java_opts}" == *-Xms* ]] || [[ "${java_opts}" == *-XX:MaxPermSize* ]] || [[ "${java_opts}" == *-XX:ReservedCodeCacheSize* ]] || [[ "${java_opts}" == *-XX:MaxMetaspaceSize* ]]; then 144 | # if we detect any of these settings in ${java_opts} we need to NOT output our settings. 145 | # The reason is the Xms/Xmx, if they don't line up, cause errors. 146 | memopts="" 147 | extmemopts="" 148 | fi 149 | 150 | echo "${memopts} ${extmemopts}" 151 | } 152 | require_arg () { 153 | local type="$1" 154 | local opt="$2" 155 | local arg="$3" 156 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 157 | die "$opt requires <$type> argument" 158 | fi 159 | } 160 | is_function_defined() { 161 | declare -f "$1" > /dev/null 162 | } 163 | 164 | # If we're *not* running in a terminal, and we don't have any arguments, then we need to add the 'ui' parameter 165 | detect_terminal_for_ui() { 166 | [[ ! -t 0 ]] && [[ "${#residual_args}" == "0" ]] && { 167 | addResidual "ui" 168 | } 169 | # SPECIAL TEST FOR MAC 170 | [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]] && [[ "${#residual_args}" == "0" ]] && { 171 | echo "Detected MAC OSX launched script...." 172 | echo "Swapping to UI" 173 | addResidual "ui" 174 | } 175 | } 176 | 177 | # Processes incoming arguments and places them in appropriate global variables. called by the run method. 178 | process_args () { 179 | while [[ $# -gt 0 ]]; do 180 | case "$1" in 181 | -h|-help) usage; exit 1 ;; 182 | -v|-verbose) verbose=1 && shift ;; 183 | -d|-debug) debug=1 && shift ;; 184 | -mem) require_arg integer "$1" "$2" && app_mem="$2" && shift 2 ;; 185 | -jvm-debug) 186 | if echo "$2" | grep -E ^[0-9]+$ > /dev/null; then 187 | addDebugger "$2" && shift 188 | else 189 | addDebugger 9999 190 | fi 191 | shift ;; 192 | -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; 193 | -D*) addJava "$1" && shift ;; 194 | -J*) addJava "${1:2}" && shift ;; 195 | *) addResidual "$1" && shift ;; 196 | esac 197 | done 198 | 199 | is_function_defined process_my_args && { 200 | myargs=("${residual_args[@]}") 201 | residual_args=() 202 | process_my_args "${myargs[@]}" 203 | } 204 | } 205 | 206 | # Actually runs the script. 207 | run() { 208 | # TODO - check for sane environment 209 | 210 | # process the combined args, then reset "$@" to the residuals 211 | process_args "$@" 212 | detect_terminal_for_ui 213 | set -- "${residual_args[@]}" 214 | argumentCount=$# 215 | 216 | #check for jline terminal fixes on cygwin 217 | if is_cygwin; then 218 | stty -icanon min 1 -echo > /dev/null 2>&1 219 | addJava "-Djline.terminal=jline.UnixTerminal" 220 | addJava "-Dsbt.cygwin=true" 221 | fi 222 | 223 | # run sbt 224 | execRunner "$java_cmd" \ 225 | "-Dactivator.home=$(make_url "$activator_home")" \ 226 | $(get_mem_opts $app_mem) \ 227 | ${java_opts[@]} \ 228 | ${java_args[@]} \ 229 | -jar "$app_launcher" \ 230 | "${app_commands[@]}" \ 231 | "${residual_args[@]}" 232 | 233 | local exit_code=$? 234 | if is_cygwin; then 235 | stty icanon echo > /dev/null 2>&1 236 | fi 237 | exit $exit_code 238 | } 239 | 240 | # Loads a configuration file full of default command line options for this script. 241 | loadConfigFile() { 242 | cat "$1" | sed '/^\#/d' 243 | } 244 | 245 | ### ------------------------------- ### 246 | ### Start of customized settings ### 247 | ### ------------------------------- ### 248 | usage() { 249 | cat < [options] 251 | 252 | Command: 253 | ui Start the Activator UI 254 | new [name] [template-id] Create a new project with [name] using template [template-id] 255 | list-templates Print all available template names 256 | -h | -help Print this message 257 | 258 | Options: 259 | -v | -verbose Make this runner chattier 260 | -d | -debug Set sbt log level to debug 261 | -mem Set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) 262 | -jvm-debug Turn on JVM debugging, open at the given port. 263 | 264 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 265 | -java-home Alternate JAVA_HOME 266 | 267 | # jvm options and output control 268 | -Dkey=val Pass -Dkey=val directly to the java runtime 269 | -J-X Pass option -X directly to the java runtime 270 | (-J is stripped) 271 | 272 | # environment variables (read from context) 273 | JAVA_OPTS Environment variable, if unset uses "" 274 | SBT_OPTS Environment variable, if unset uses "" 275 | ACTIVATOR_OPTS Environment variable, if unset uses "" 276 | 277 | In the case of duplicated or conflicting options, the order above 278 | shows precedence: environment variables lowest, command line options highest. 279 | EOM 280 | } 281 | 282 | ### ------------------------------- ### 283 | ### Main script ### 284 | ### ------------------------------- ### 285 | 286 | declare -a residual_args 287 | declare -a java_args 288 | declare -a app_commands 289 | declare -r real_script_path="$(realpath "$0")" 290 | declare -r activator_home="$(realpath "$(dirname "$real_script_path")")" 291 | declare -r app_version="1.3.5" 292 | 293 | declare -r app_launcher="${activator_home}/activator-launch-${app_version}.jar" 294 | declare -r script_name=activator 295 | java_cmd=$(get_java_cmd) 296 | declare -r java_opts=( "${ACTIVATOR_OPTS[@]}" "${SBT_OPTS[@]}" "${JAVA_OPTS[@]}" "${java_opts[@]}" ) 297 | userhome="$HOME" 298 | if is_cygwin; then 299 | # cygwin sets home to something f-d up, set to real windows homedir 300 | userhome="$USERPROFILE" 301 | fi 302 | declare -r activator_user_home_dir="${userhome}/.activator" 303 | declare -r java_opts_config_home="${activator_user_home_dir}/activatorconfig.txt" 304 | declare -r java_opts_config_version="${activator_user_home_dir}/${app_version}/activatorconfig.txt" 305 | 306 | # Now check to see if it's a good enough version 307 | declare -r java_version=$("$java_cmd" -version 2>&1 | awk -F '"' '/version/ {print $2}') 308 | if [[ "$java_version" == "" ]]; then 309 | echo 310 | echo No java installations was detected. 311 | echo Please go to http://www.java.com/getjava/ and download 312 | echo 313 | exit 1 314 | elif [[ ! "$java_version" > "1.6" ]]; then 315 | echo 316 | echo The java installation you have is not up to date 317 | echo Activator requires at least version 1.6+, you have 318 | echo version $java_version 319 | echo 320 | echo Please go to http://www.java.com/getjava/ and download 321 | echo a valid Java Runtime and install before running Activator. 322 | echo 323 | exit 1 324 | fi 325 | 326 | # if configuration files exist, prepend their contents to the java args so it can be processed by this runner 327 | # a "versioned" config trumps one on the top level 328 | if [[ -f "$java_opts_config_version" ]]; then 329 | addConfigOpts $(loadConfigFile "$java_opts_config_version") 330 | elif [[ -f "$java_opts_config_home" ]]; then 331 | addConfigOpts $(loadConfigFile "$java_opts_config_home") 332 | fi 333 | 334 | run "$@" 335 | -------------------------------------------------------------------------------- /activator-launch-1.3.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvolpe/light-play-rest-api/fad5b0c6fe123f94a1588b8ac507b05a217bcaff/activator-launch-1.3.5.jar -------------------------------------------------------------------------------- /activator.bat: -------------------------------------------------------------------------------- 1 | @REM activator launcher script 2 | @REM 3 | @REM Environment: 4 | @REM In order for Activator to work you must have Java available on the classpath 5 | @REM JAVA_HOME - location of a JDK home dir (optional if java on path) 6 | @REM CFG_OPTS - JVM options (optional) 7 | @REM Configuration: 8 | @REM activatorconfig.txt found in the ACTIVATOR_HOME or ACTIVATOR_HOME/ACTIVATOR_VERSION 9 | @setlocal enabledelayedexpansion 10 | 11 | @echo off 12 | 13 | set "var1=%~1" 14 | if defined var1 ( 15 | if "%var1%"=="help" ( 16 | echo. 17 | echo Usage activator [options] [command] 18 | echo. 19 | echo Commands: 20 | echo ui Start the Activator UI 21 | echo new [name] [template-id] Create a new project with [name] using template [template-id] 22 | echo list-templates Print all available template names 23 | echo help Print this message 24 | echo. 25 | echo Options: 26 | echo -jvm-debug [port] Turn on JVM debugging, open at the given port. Defaults to 9999 if no port given. 27 | echo. 28 | echo Environment variables ^(read from context^): 29 | echo JAVA_OPTS Environment variable, if unset uses "" 30 | echo SBT_OPTS Environment variable, if unset uses "" 31 | echo ACTIVATOR_OPTS Environment variable, if unset uses "" 32 | echo. 33 | echo Please note that in order for Activator to work you must have Java available on the classpath 34 | echo. 35 | goto :end 36 | ) 37 | ) 38 | 39 | if "%ACTIVATOR_HOME%"=="" ( 40 | set "ACTIVATOR_HOME=%~dp0" 41 | @REM remove trailing "\" from path 42 | set ACTIVATOR_HOME=!ACTIVATOR_HOME:~0,-1! 43 | ) 44 | 45 | set ERROR_CODE=0 46 | set APP_VERSION=1.3.5 47 | set ACTIVATOR_LAUNCH_JAR=activator-launch-%APP_VERSION%.jar 48 | 49 | rem Detect if we were double clicked, although theoretically A user could 50 | rem manually run cmd /c 51 | for %%x in (%cmdcmdline%) do if %%~x==/c set DOUBLECLICKED=1 52 | 53 | rem FIRST we load a config file of extra options (if there is one) 54 | set "CFG_FILE_HOME=%UserProfile%\.activator\activatorconfig.txt" 55 | set "CFG_FILE_VERSION=%UserProfile%\.activator\%APP_VERSION%\activatorconfig.txt" 56 | set CFG_OPTS= 57 | if exist %CFG_FILE_VERSION% ( 58 | FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%CFG_FILE_VERSION%") DO ( 59 | set DO_NOT_REUSE_ME=%%i 60 | rem ZOMG (Part #2) WE use !! here to delay the expansion of 61 | rem CFG_OPTS, otherwise it remains "" for this loop. 62 | set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME! 63 | ) 64 | ) 65 | if "%CFG_OPTS%"=="" ( 66 | if exist %CFG_FILE_HOME% ( 67 | FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%CFG_FILE_HOME%") DO ( 68 | set DO_NOT_REUSE_ME=%%i 69 | rem ZOMG (Part #2) WE use !! here to delay the expansion of 70 | rem CFG_OPTS, otherwise it remains "" for this loop. 71 | set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME! 72 | ) 73 | ) 74 | ) 75 | 76 | rem We use the value of the JAVACMD environment variable if defined 77 | set _JAVACMD=%JAVACMD% 78 | 79 | if "%_JAVACMD%"=="" ( 80 | if not "%JAVA_HOME%"=="" ( 81 | if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe" 82 | 83 | rem if there is a java home set we make sure it is the first picked up when invoking 'java' 84 | SET "PATH=%JAVA_HOME%\bin;%PATH%" 85 | ) 86 | ) 87 | 88 | if "%_JAVACMD%"=="" set _JAVACMD=java 89 | 90 | rem Detect if this java is ok to use. 91 | for /F %%j in ('"%_JAVACMD%" -version 2^>^&1') do ( 92 | if %%~j==java set JAVAINSTALLED=1 93 | if %%~j==openjdk set JAVAINSTALLED=1 94 | ) 95 | 96 | rem Detect the same thing about javac 97 | if "%_JAVACCMD%"=="" ( 98 | if not "%JAVA_HOME%"=="" ( 99 | if exist "%JAVA_HOME%\bin\javac.exe" set "_JAVACCMD=%JAVA_HOME%\bin\javac.exe" 100 | ) 101 | ) 102 | if "%_JAVACCMD%"=="" set _JAVACCMD=javac 103 | for /F %%j in ('"%_JAVACCMD%" -version 2^>^&1') do ( 104 | if %%~j==javac set JAVACINSTALLED=1 105 | ) 106 | 107 | rem BAT has no logical or, so we do it OLD SCHOOL! Oppan Redmond Style 108 | set JAVAOK=true 109 | if not defined JAVAINSTALLED set JAVAOK=false 110 | if not defined JAVACINSTALLED set JAVAOK=false 111 | 112 | if "%JAVAOK%"=="false" ( 113 | echo. 114 | echo A Java JDK is not installed or can't be found. 115 | if not "%JAVA_HOME%"=="" ( 116 | echo JAVA_HOME = "%JAVA_HOME%" 117 | ) 118 | echo. 119 | echo Please go to 120 | echo http://www.oracle.com/technetwork/java/javase/downloads/index.html 121 | echo and download a valid Java JDK and install before running Activator. 122 | echo. 123 | echo If you think this message is in error, please check 124 | echo your environment variables to see if "java.exe" and "javac.exe" are 125 | echo available via JAVA_HOME or PATH. 126 | echo. 127 | if defined DOUBLECLICKED pause 128 | exit /B 1 129 | ) 130 | 131 | rem Check what Java version is being used to determine what memory options to use 132 | for /f "tokens=3" %%g in ('java -version 2^>^&1 ^| findstr /i "version"') do ( 133 | set JAVA_VERSION=%%g 134 | ) 135 | 136 | rem Strips away the " characters 137 | set JAVA_VERSION=%JAVA_VERSION:"=% 138 | 139 | rem TODO Check if there are existing mem settings in JAVA_OPTS/CFG_OPTS and use those instead of the below 140 | for /f "delims=. tokens=1-3" %%v in ("%JAVA_VERSION%") do ( 141 | set MAJOR=%%v 142 | set MINOR=%%w 143 | set BUILD=%%x 144 | 145 | set META_SIZE=-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=256M 146 | if "!MINOR!" LSS "8" ( 147 | set META_SIZE=-XX:PermSize=64M -XX:MaxPermSize=256M 148 | ) 149 | 150 | set MEM_OPTS=!META_SIZE! 151 | ) 152 | 153 | rem We use the value of the JAVA_OPTS environment variable if defined, rather than the config. 154 | set _JAVA_OPTS=%JAVA_OPTS% 155 | if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%CFG_OPTS% 156 | 157 | set DEBUG_OPTS= 158 | 159 | rem Loop through the arguments, building remaining args in args variable 160 | set args= 161 | :argsloop 162 | if not "%~1"=="" ( 163 | rem Checks if the argument contains "-D" and if true, adds argument 1 with 2 and puts an equal sign between them. 164 | rem This is done since batch considers "=" to be a delimiter so we need to circumvent this behavior with a small hack. 165 | set arg1=%~1 166 | if "!arg1:~0,2!"=="-D" ( 167 | set "args=%args% "%~1"="%~2"" 168 | shift 169 | shift 170 | goto argsloop 171 | ) 172 | 173 | if "%~1"=="-jvm-debug" ( 174 | if not "%~2"=="" ( 175 | rem This piece of magic somehow checks that an argument is a number 176 | for /F "delims=0123456789" %%i in ("%~2") do ( 177 | set var="%%i" 178 | ) 179 | if defined var ( 180 | rem Not a number, assume no argument given and default to 9999 181 | set JPDA_PORT=9999 182 | ) else ( 183 | rem Port was given, shift arguments 184 | set JPDA_PORT=%~2 185 | shift 186 | ) 187 | ) else ( 188 | set JPDA_PORT=9999 189 | ) 190 | shift 191 | 192 | set DEBUG_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=!JPDA_PORT! 193 | goto argsloop 194 | ) 195 | rem else 196 | set "args=%args% "%~1"" 197 | shift 198 | goto argsloop 199 | ) 200 | 201 | :run 202 | 203 | if "!args!"=="" ( 204 | if defined DOUBLECLICKED ( 205 | set CMDS="ui" 206 | ) else set CMDS=!args! 207 | ) else set CMDS=!args! 208 | 209 | rem We add a / in front, so we get file:///C: instead of file://C: 210 | rem Java considers the later a UNC path. 211 | rem We also attempt a solid effort at making it URI friendly. 212 | rem We don't even bother with UNC paths. 213 | set JAVA_FRIENDLY_HOME_1=/!ACTIVATOR_HOME:\=/! 214 | set JAVA_FRIENDLY_HOME=/!JAVA_FRIENDLY_HOME_1: =%%20! 215 | 216 | rem Checks if the command contains spaces to know if it should be wrapped in quotes or not 217 | set NON_SPACED_CMD=%_JAVACMD: =% 218 | if "%_JAVACMD%"=="%NON_SPACED_CMD%" %_JAVACMD% %DEBUG_OPTS% %MEM_OPTS% %ACTIVATOR_OPTS% %SBT_OPTS% %_JAVA_OPTS% "-Dactivator.home=%JAVA_FRIENDLY_HOME%" -jar "%ACTIVATOR_HOME%\%ACTIVATOR_LAUNCH_JAR%" %CMDS% 219 | if NOT "%_JAVACMD%"=="%NON_SPACED_CMD%" "%_JAVACMD%" %DEBUG_OPTS% %MEM_OPTS% %ACTIVATOR_OPTS% %SBT_OPTS% %_JAVA_OPTS% "-Dactivator.home=%JAVA_FRIENDLY_HOME%" -jar "%ACTIVATOR_HOME%\%ACTIVATOR_LAUNCH_JAR%" %CMDS% 220 | 221 | if ERRORLEVEL 1 goto error 222 | goto end 223 | 224 | :error 225 | set ERROR_CODE=1 226 | 227 | :end 228 | 229 | @endlocal 230 | 231 | exit /B %ERROR_CODE% 232 | -------------------------------------------------------------------------------- /app/Boot.scala: -------------------------------------------------------------------------------- 1 | import play.api.ApplicationLoader.Context 2 | import play.api.routing.Router 3 | import play.api.{Application, ApplicationLoader, BuiltInComponentsFromContext} 4 | import routers.{OrdersRouter, ProductsRouter, UsersRouter} 5 | 6 | class Boot extends ApplicationLoader { 7 | 8 | def load(context: Context): Application = new BuiltInComponentsFromContext(context) { 9 | def router: Router = Router.from { 10 | UsersRouter() orElse 11 | ProductsRouter() orElse 12 | OrdersRouter() 13 | } 14 | }.application 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/models/Model.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import play.api.libs.json.Json 4 | 5 | object Model { 6 | 7 | case class User(id: Long, name: String) 8 | case class Product(id: Long, name: String, price: Double) 9 | 10 | object Implicits { 11 | 12 | implicit val userFormat = Json.format[User] 13 | implicit val productFormat = Json.format[Product] 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/repository/InMemoryData.scala: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import models.Model.User 4 | 5 | import scala.collection.mutable 6 | 7 | object InMemoryData { 8 | 9 | val users = mutable.HashMap[Long, User]() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/repository/UsersRepository.scala: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import models.Model.User 4 | import play.api.libs.concurrent.Execution.Implicits._ 5 | import scala.concurrent.Future 6 | 7 | trait UsersRepository { 8 | 9 | def find(id: Long): Future[Option[User]] 10 | 11 | def save(user: User): Future[Unit] 12 | 13 | } 14 | 15 | class DefaultUsersRepository extends UsersRepository { 16 | 17 | def find(id: Long): Future[Option[User]] = Future { 18 | InMemoryData.users.get(id) 19 | } 20 | 21 | def save(user: User): Future[Unit] = Future { 22 | InMemoryData.users.put(user.id, user) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/routers/OrdersRouter.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import play.api.libs.concurrent.Execution.Implicits._ 4 | import play.api.mvc.Action 5 | import play.api.mvc.Results._ 6 | import play.api.routing.Router 7 | import play.api.routing.sird._ 8 | import scala.concurrent.Future 9 | 10 | object OrdersRouter extends OrdersRouter { 11 | def apply() : Router.Routes = routes 12 | } 13 | 14 | trait OrdersRouter { 15 | 16 | def routes : Router.Routes = { 17 | 18 | case GET(p"/users/$id/orders") => Action.async { 19 | Future(Ok(s"Orders of the user with id: $id")) 20 | } 21 | 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/routers/ProductsRouter.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import play.api.libs.concurrent.Execution.Implicits._ 4 | import play.api.mvc.Action 5 | import play.api.mvc.Results._ 6 | import play.api.routing.Router 7 | import play.api.routing.sird._ 8 | import scala.concurrent.Future 9 | 10 | object ProductsRouter extends ProductsRouter { 11 | def apply(): Router.Routes = routes 12 | } 13 | 14 | trait ProductsRouter { 15 | 16 | def routes : Router.Routes = { 17 | 18 | // Using optional parameters 19 | case GET(p"/products") => Action.async { implicit request => 20 | val order = request.getQueryString("order").getOrElse("asc") 21 | Future(Ok(s"List of Products with optional parameter order=$order")) 22 | } 23 | 24 | case GET(p"/products/$id") => Action.async { 25 | Future(Ok(s"Product ID: $id")) 26 | } 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/routers/UsersRouter.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import models.Model.Implicits._ 4 | import models.Model.User 5 | import play.api.mvc.BodyParsers.parse 6 | import play.api.libs.concurrent.Execution.Implicits._ 7 | import play.api.libs.json._ 8 | import play.api.mvc.Action 9 | import play.api.mvc.Results._ 10 | import play.api.routing.Router 11 | import play.api.routing.sird._ 12 | import repository.{DefaultUsersRepository, UsersRepository} 13 | 14 | object UsersRouter extends DefaultUsersRepository with UsersRouter { 15 | def apply(): Router.Routes = routes 16 | } 17 | 18 | trait UsersRouter { 19 | 20 | self: UsersRepository => 21 | 22 | def routes: Router.Routes = { 23 | 24 | case GET(p"/users/${long(id)}") => Action.async { 25 | find(id) map { 26 | case Some(user) => Ok(Json.toJson(user)) 27 | case None => NotFound 28 | } 29 | } 30 | 31 | case POST(p"/users") => Action.async(parse.json[User]) { implicit request => 32 | val user = request.body 33 | save(user) map (_ => Created) 34 | } 35 | 36 | } 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("light-play-rest-api") { 4 | 5 | @play20.welcome(message) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 |

light-play-rest-api

11 | 12 | 13 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := """light-play-rest-api""" 2 | 3 | version := "1.0-SNAPSHOT" 4 | 5 | lazy val root = (project in file(".")).enablePlugins(PlayScala) 6 | 7 | scalaVersion := "2.11.7" 8 | 9 | libraryDependencies ++= Seq( 10 | ws, 11 | specs2 % Test 12 | ) 13 | 14 | resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases" 15 | 16 | // Play provides two styles of routers, one expects its actions to be injected, the 17 | // other, legacy style, accesses its actions statically. 18 | routesGenerator := InjectedRoutesGenerator 19 | 20 | ScoverageSbtPlugin.ScoverageKeys.coverageExcludedPackages := ".*index.*;.*main.*" 21 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | play.crypto.secret = "changeme" 12 | 13 | # The application languages 14 | # ~~~~~ 15 | play.i18n.langs = [ "en" ] 16 | 17 | # Router 18 | # ~~~~~ 19 | # Define the Router object to use for this application. 20 | # This router will be looked up first when the application is starting up, 21 | # so make sure this is the entry point. 22 | # Furthermore, it's assumed your route file is named properly. 23 | # So for an application router like `my.application.Router`, 24 | # you may need to define a router file `conf/my.application.routes`. 25 | # Default to Routes in the root package (and conf/routes) 26 | # play.http.router = my.application.Routes 27 | 28 | # Database configuration 29 | # ~~~~~ 30 | # You can declare as many datasources as you want. 31 | # By convention, the default datasource is named `default` 32 | # 33 | # db.default.driver=org.h2.Driver 34 | # db.default.url="jdbc:h2:mem:play" 35 | # db.default.username=sa 36 | # db.default.password="" 37 | 38 | # Evolutions 39 | # ~~~~~ 40 | # You can disable evolutions if needed 41 | # play.evolutions.enabled=false 42 | 43 | # You can disable evolutions for a specific datasource if necessary 44 | # play.evolutions.db.default.enabled=false 45 | 46 | play.application.loader = Boot 47 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %coloredLevel - %logger - %message%n%xException 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Classpaths.sbtPluginReleases 2 | resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/" 3 | 4 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.2") 5 | 6 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.0.4") 7 | 8 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.0.0.BETA1") -------------------------------------------------------------------------------- /test/routers/BaseRouterSpecification.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import play.api.ApplicationLoader.Context 4 | import play.api.routing.Router 5 | import play.api.test.PlaySpecification 6 | import play.api.{Application, ApplicationLoader, BuiltInComponentsFromContext} 7 | import repository.DefaultUsersRepository 8 | 9 | abstract class BaseRouterSpecification extends PlaySpecification { 10 | 11 | object FakeUsersRouter extends DefaultUsersRepository with UsersRouter { 12 | def apply(): Router.Routes = routes 13 | } 14 | 15 | val fakeAppLoader = new ApplicationLoader() { 16 | def load(context: Context): Application = new BuiltInComponentsFromContext(context) { 17 | def router = Router.from { 18 | List(FakeUsersRouter(), ProductsRouter(), OrdersRouter()) reduceLeft (_ orElse _) 19 | } 20 | }.application 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /test/routers/OrdersRouterSpec.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import play.api.routing.Router 4 | import play.api.test._ 5 | 6 | class OrdersRouterSpec extends BaseRouterSpecification { 7 | 8 | // In the near future it will be changed by a fake router 9 | val fakeRouter = Router.from(OrdersRouter()) 10 | 11 | "Orders Router" should { 12 | 13 | "Find the order for the user" in new WithApplicationLoader(fakeAppLoader) { 14 | val fakeRequest = FakeRequest(GET, "/users/123/orders") 15 | val Some(result) = route(fakeRequest) 16 | 17 | status(result) must equalTo(OK) 18 | } 19 | 20 | "Have a specific handler to GET a Product by id" in new WithApplication() { 21 | val fakeRequest = FakeRequest(GET, "/users/123/orders") 22 | val handler = fakeRouter.handlerFor(fakeRequest) 23 | 24 | handler must be_!=(None) 25 | } 26 | 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /test/routers/ProductsRouterSpec.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import play.api.routing.Router 4 | import play.api.test._ 5 | 6 | class ProductsRouterSpec extends BaseRouterSpecification { 7 | 8 | // In the near future it will be changed by a fake router 9 | val fakeRouter = Router.from(ProductsRouter()) 10 | 11 | "Products Router" should { 12 | 13 | "Find product by Id" in new WithApplicationLoader(fakeAppLoader) { 14 | val fakeRequest = FakeRequest(GET, "/products/123") 15 | val Some(result) = route(fakeRequest) 16 | 17 | status(result) must equalTo(OK) 18 | } 19 | 20 | "Find all products" in new WithApplicationLoader(fakeAppLoader) { 21 | val fakeRequest = FakeRequest(GET, "/products") 22 | val Some(result) = route(fakeRequest) 23 | 24 | status(result) must equalTo(OK) 25 | contentAsString(result) must contain("order=asc") //Default value 26 | } 27 | 28 | "Find all products specifying the order" in new WithApplicationLoader(fakeAppLoader) { 29 | val fakeRequest = FakeRequest(GET, "/products?order=desc") 30 | val Some(result) = route(fakeRequest) 31 | 32 | status(result) must equalTo(OK) 33 | contentAsString(result) must contain("order=desc") //Optional parameter 34 | } 35 | 36 | "Have a specific handler to GET a Product by id" in new WithApplication() { 37 | val fakeRequest = FakeRequest(GET, "/products/87") 38 | val handler = fakeRouter.handlerFor(fakeRequest) 39 | 40 | handler must be_!=(None) 41 | } 42 | 43 | "Have a specific handler to GET All Products" in new WithApplication() { 44 | val fakeRequest = FakeRequest(GET, "/products") 45 | val handler = fakeRouter.handlerFor(fakeRequest) 46 | 47 | handler must be_!=(None) 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /test/routers/UsersRouterSpec.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import models.Model.Implicits._ 4 | import models.Model.User 5 | import play.api.libs.json._ 6 | import play.api.routing.Router 7 | import play.api.test._ 8 | 9 | class UsersRouterSpec extends BaseRouterSpecification { 10 | 11 | val fakeRouter = Router.from(FakeUsersRouter()) 12 | 13 | "Users Router" should { 14 | 15 | "Not find the user" in new WithApplicationLoader(fakeAppLoader) { 16 | val fakeRequest = FakeRequest(GET, "/users/123") 17 | val Some(result) = route(fakeRequest) 18 | 19 | status(result) must equalTo(NOT_FOUND) 20 | } 21 | 22 | "Create and Find the user" in new WithApplicationLoader(fakeAppLoader) { 23 | val body = Json.toJson(User(87, "Gabriel")) 24 | val fakePost = FakeRequest(POST, "/users").withJsonBody(body) 25 | val Some(postResult) = route(fakePost) 26 | 27 | status(postResult) must equalTo(CREATED) 28 | 29 | val fakeRequest = FakeRequest(GET, "/users/87") 30 | val Some(result) = route(fakeRequest) 31 | 32 | status(result) must equalTo(OK) 33 | contentType(result) must beSome("application/json") 34 | contentAsJson(result) must equalTo(body) 35 | } 36 | 37 | "Have a specific handler to GET an User by id" in new WithApplication() { 38 | val fakeRequest = FakeRequest(GET, "/users/87") 39 | val handler = fakeRouter.handlerFor(fakeRequest) 40 | 41 | handler must be_!=(None) 42 | } 43 | 44 | "Have a specific handler to CREATE an User" in new WithApplication() { 45 | val body = User(87, "Gabriel") 46 | val fakeRequest = FakeRequest(POST, "/users").withBody(body) 47 | val handler = fakeRouter.handlerFor(fakeRequest) 48 | 49 | handler must be_!=(None) 50 | } 51 | 52 | "Have no handler" in new WithApplication() { 53 | val fakeRequest = FakeRequest(GET, "") 54 | val handler = fakeRouter.handlerFor(fakeRequest) 55 | 56 | handler must be_==(None) 57 | } 58 | 59 | } 60 | 61 | } 62 | --------------------------------------------------------------------------------