diff --git a/CHANGELOG b/CHANGELOG index 92d6d016ec..101183b72d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ v1.3.0-alpha1 (XXXX-XX-XX) -------------------------- +* added API for user-defined AQL functions + * issue #475: A better error message for deleting a non-existent graph * issue #474: Web interface problems with the JS Shell diff --git a/Documentation/Examples/api-aqlfunction-create b/Documentation/Examples/api-aqlfunction-create new file mode 100644 index 0000000000..4d7a211a3d --- /dev/null +++ b/Documentation/Examples/api-aqlfunction-create @@ -0,0 +1,10 @@ +> curl --data @- -X POST --dump - http://localhost:8529/_api/aqlfunction +{ "name" : "myfunctions:temperature:celsiustofahrenheit, "code" : "function (celsius) { return celsius * 1.8 + 32; }" } + +HTTP/1.1 201 Created +content-type: application/json; charset=utf-8 + +{ + "error" : false, + "code" : 201 +} diff --git a/Documentation/ImplementorManual/HttpAqlFunctions.md b/Documentation/ImplementorManual/HttpAqlFunctions.md new file mode 100644 index 0000000000..5c982a6519 --- /dev/null +++ b/Documentation/ImplementorManual/HttpAqlFunctions.md @@ -0,0 +1,34 @@ +HTTP Interface for AQL User Functions Management {#HttpAqlFunctions} +==================================================================== + +@NAVIGATE_HttpAqlFunctions +@EMBEDTOC{HttpAqlFunctionsTOC} + +AQL User Functions Management {#HttpAqlFunctionsIntro} +====================================================== + +This is an introduction to ArangoDB's Http interface for managing AQL +user functions. AQL user functions are a means to extend the functionality +of ArangoDB's query language (AQL) with user-defined Javascript code. + +For an overview of how AQL user functions work, please refer to @ref +ExtendingAql. + +The Http interface provides an API for adding, deleting, and listing +previously registered AQL user functions. + +All user functions managed through this interface will be stored in the +system collection `_aqlfunctions`. Documents in this collection should not +be accessed directly, but only via the dedicated interfaces. + +@anchor HttpAqlFunctionsSave +@copydetails JSF_POST_api_aqlfunction + +@CLEARPAGE +@anchor HttpAqlFunctionsRemove +@copydetails JSF_DELETE_api_aqlfunction + +@CLEARPAGE +@anchor HttpAqlFunctionsList +@copydetails JSF_GET_api_aqlfunction + diff --git a/Documentation/ImplementorManual/HttpAqlFunctionsTOC.md b/Documentation/ImplementorManual/HttpAqlFunctionsTOC.md new file mode 100644 index 0000000000..d02af75a94 --- /dev/null +++ b/Documentation/ImplementorManual/HttpAqlFunctionsTOC.md @@ -0,0 +1,8 @@ +TOC {#HttpAqlFunctionsTOC} +========================== + +- @ref HttpAqlFunctions + - @ref HttpAqlFunctionsIntro + - @ref HttpAqlFunctionsSave "POST /_api/aqlfunction" + - @ref HttpAqlFunctionsRemove "DELETE /_api/aqlfunction/functionname" + - @ref HttpAqlFunctionsList "GET /_api/aqlfunction" diff --git a/Documentation/ImplementorManual/ImplementorManual.md b/Documentation/ImplementorManual/ImplementorManual.md index c561149ce5..2864314e44 100644 --- a/Documentation/ImplementorManual/ImplementorManual.md +++ b/Documentation/ImplementorManual/ImplementorManual.md @@ -7,6 +7,7 @@ ArangoDB for API Implementors (@VERSION) {#ImplementorManual} @CHAPTER_REF{RestEdge} @CHAPTER_REF{HttpCursor} @CHAPTER_REF{HttpQuery} +@CHAPTER_REF{HttpAqlFunctions} @CHAPTER_REF{HttpSimple} @CHAPTER_REF{HttpCollection} @CHAPTER_REF{HttpIndex} diff --git a/Documentation/Makefile.files b/Documentation/Makefile.files index 4238b48d28..dc357d234d 100644 --- a/Documentation/Makefile.files +++ b/Documentation/Makefile.files @@ -33,6 +33,7 @@ dist_man_MANS += \ ################################################################################ DOXYGEN = \ + Doxygen/js/actions/api-aqlfunction.c \ Doxygen/js/actions/api-collection.c \ Doxygen/js/actions/api-cursor.c \ Doxygen/js/actions/api-edges.c \ @@ -49,6 +50,7 @@ DOXYGEN = \ Doxygen/js/common/bootstrap/module-fs.c \ Doxygen/js/common/bootstrap/modules.c \ Doxygen/js/common/modules/jsunity.c \ + Doxygen/js/common/modules/org/arangodb/aql/functions.c \ Doxygen/js/common/modules/org/arangodb/arango-collection-common.c \ Doxygen/js/common/modules/org/arangodb/arango-statement-common.c \ Doxygen/js/common/modules/org/arangodb/graph-common.c \ @@ -78,6 +80,7 @@ WIKI = \ DbaManualBasics \ DbaManualDatafileDebugger \ DbaManualEmergencyConsole \ + ExtendingAql \ FirstStepsArangoDB \ Glossary \ HandlingCollections \ @@ -85,6 +88,7 @@ WIKI = \ HandlingEdges \ HandlingIndexes \ Home \ + HttpAqlFunctions \ HttpBatch \ HttpCollection \ HttpCursor \ @@ -155,6 +159,7 @@ Doxygen/.setup-directories: @test -d Doxygen/website/images || mkdir -p Doxygen/website/images @test -d Doxygen/js/actions || mkdir -p Doxygen/js/actions @test -d Doxygen/js/common/bootstrap || mkdir -p Doxygen/js/common/bootstrap + @test -d Doxygen/js/common/modules/org/arangodb/aql || mkdir -p Doxygen/js/common/modules/org/arangodb/aql @test -d Doxygen/js/common/modules/org/arangodb || mkdir -p Doxygen/js/common/modules/org/arangodb @test -d Doxygen/js/server/modules/org/arangodb || mkdir -p Doxygen/js/server/modules/org/arangodb @test -d Doxygen/js/client || mkdir -p Doxygen/js/client diff --git a/Documentation/UserManual/Aql.md b/Documentation/UserManual/Aql.md index 6acb32437c..5c07cb3e47 100644 --- a/Documentation/UserManual/Aql.md +++ b/Documentation/UserManual/Aql.md @@ -779,7 +779,7 @@ AQL supports functions to allow more complex computations. Functions can be called at any query position where an expression is allowed. The general function call syntax is: - FUNCTIONAME(arguments) + FUNCTIONNAME(arguments) where `FUNCTIONNAME` is the name of the function to be called, and `arguments` is a comma-separated list of function arguments. If a function does not need any @@ -793,7 +793,30 @@ Some example function calls: LENGTH(friends) COLLECTIONS() -Function names are not case-sensitive. +In contrast to collection and variable names, function names are case-insensitive, +i.e. `LENGTH(foo)` and `length(foo)` are equivalent. + +@subsubsection AqlFunctionsExtending Extending AQL + +Since ArangoDB 1.3, it is possible to extend AQL with user-defined functions. +These functions need to be written in Javascript, and be registered before usage +in a query. + +Please refer to @ref ExtendingAql for more details on this. + +By default, any function used in an AQL query will be sought in the built-in +function namespace `_aql`. This is the default namespace that contains all AQL +functions that are shipped with ArangoDB. +To refer to a user-defined AQL function, the function name must be fully qualified +to also include the user-defined namespace. The `:` symbol is used as the namespace +separator: + + MYGROUP:MYFUNC() + + MYFUNCTIONS.MATH.RANDOM() + +As all AQL function names, user function names are also case-insensitive. + @subsubsection AqlFunctionsCasting Type cast functions diff --git a/Documentation/UserManual/ExtendingAql.md b/Documentation/UserManual/ExtendingAql.md new file mode 100644 index 0000000000..b49ff7cf28 --- /dev/null +++ b/Documentation/UserManual/ExtendingAql.md @@ -0,0 +1,81 @@ +Extending AQL with User Functions {#ExtendingAql} +================================================= + +@NAVIGATE_ExtendingAql +@EMBEDTOC{ExtendingAqlTOC} + +Conventions {#ExtendingAqlConventions} +====================================== + +AQL comes with a built-in set of functions, but it is no +full-feature programming language. + +To add missing functionality or to simplifiy queries, users +may add own functions to AQL. These functions can be written +in Javascript, and must be registered via an API. + +In order to avoid conflicts with existing or future built-in +function names, all user functions must be put into separate +namespaces. Invoking a user functions is then possible by referring +to the fully-qualified function name, which includes the namespace, +too. + +The `:` symbol is used inside AQL as the namespace separator. Using +the namespace separator, users can create a multi-level hierarchy of +function groups if required. + +Examples: + + RETURN myfunctions:myfunc() + + RETURN myfunctions:math:random() + +Note: as all function names in AQL, user function names are also +case-insensitive. + +Built-in AQL functions reside in the namespace `_aql`, which is also +the default namespace to look in if an unqualified function name is +found. Adding user functions to the `_aql` namespace is disallowed and +will fail. + +User functions can take any number of input arguments and should +provide one result. They should be kept purely functional and thus free of +side effects and state. + +User function code is late-bound, and may thus not rely on any variables +that existed at the time of declaration. If user function code requires +access to any external data, it must take care to set up the data by +itself. + +User functions must only return primitive types (i.e. `null`, boolean +values, numeric values, string values) or aggregate types (lists or +documents) composed of these types. +Returning any other Javascript object type from a user function may lead +to undefined behavior and should be avoided. + + +Registering and Unregistering User Functions {#ExtendingAqlHowTo} +================================================================= + +AQL user functions can be registered using the `aqlfunctions` object as +follows: + + var aqlfunctions = require("org/arangodb/aql/functions"); + +To register a function, the fully qualified function name plus the +function code must be specified. + +@copydetails JSF_aqlfunctions_register + +It is possible to unregister a single user function, or a whole group of +user functions (identified by their namespace) in one go: + +@copydetails JSF_aqlfunctions_unregister + +@copydetails JSF_aqlfunctions_unregister_group + +To get an overview of which functions are currently registered, the +`toArray` function can be used: + +@copydetails JSF_aqlfunctions_toArray + diff --git a/Documentation/UserManual/ExtendingAqlTOC.md b/Documentation/UserManual/ExtendingAqlTOC.md new file mode 100644 index 0000000000..3ab11b411d --- /dev/null +++ b/Documentation/UserManual/ExtendingAqlTOC.md @@ -0,0 +1,6 @@ +TOC {#ExtendingAqlTOC} +====================== + +- @ref ExtendingAql + - @ref ExtendingAqlConventions + - @ref ExtendingAqlHowTo diff --git a/Documentation/UserManual/UserManual.md b/Documentation/UserManual/UserManual.md index c12de49bee..f5b8f7ffba 100644 --- a/Documentation/UserManual/UserManual.md +++ b/Documentation/UserManual/UserManual.md @@ -11,6 +11,7 @@ ArangoDB's User Manual (@VERSION) {#UserManual} @CHAPTER_REF{HandlingEdges} @CHAPTER_REF{SimpleQueries} @CHAPTER_REF{Aql} +@CHAPTER_REF{ExtendingAql} @CHAPTER_REF{AqlExamples} @CHAPTER_REF{UserManualActions} @CHAPTER_REF{Transactions} diff --git a/UnitTests/HttpInterface/api-aqlfunction-spec.rb b/UnitTests/HttpInterface/api-aqlfunction-spec.rb new file mode 100644 index 0000000000..d2959c971a --- /dev/null +++ b/UnitTests/HttpInterface/api-aqlfunction-spec.rb @@ -0,0 +1,192 @@ +# coding: utf-8 + +require 'rspec' +require './arangodb.rb' + +describe ArangoDB do + api = "/_api/aqlfunction" + prefix = "api-aqlfunction" + + context "AQL user functions:" do + +################################################################################ +## error handling +################################################################################ + + context "error handling" do + + it "add function, without name" do + body = "{ \"code\" : \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-no-name", api, :body => body) + + doc.code.should eq(400) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(400) + doc.parsed_response['errorNum'].should eq(1580) + end + + it "add function, invalid name" do + body = "{ \"name\" : \"\", \"code\" : \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-invalid1", api, :body => body) + + doc.code.should eq(400) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(400) + doc.parsed_response['errorNum'].should eq(1580) + end + + it "add function, invalid name" do + body = "{ \"name\" : \"_aql:foobar\", \"code\" : \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-invalid2", api, :body => body) + + doc.code.should eq(400) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(400) + doc.parsed_response['errorNum'].should eq(1580) + end + + it "add function, invalid name" do + body = "{ \"name\" : \"foobar\", \"code\" : \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-invalid3", api, :body => body) + + doc.code.should eq(400) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(400) + doc.parsed_response['errorNum'].should eq(1580) + end + + it "add function, no code" do + body = "{ \"name\" : \"myfunc:mytest\" }" + doc = ArangoDB.log_post("#{prefix}-add-no-code", api, :body => body) + + doc.code.should eq(400) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(400) + doc.parsed_response['errorNum'].should eq(1581) + end + + it "add function, invalid code" do + body = "{ \"name\" : \"myfunc:mytest\", \"code\" : \"function ()\" }" + doc = ArangoDB.log_post("#{prefix}-add-invalid-code", api, :body => body) + + doc.code.should eq(400) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(400) + doc.parsed_response['errorNum'].should eq(1581) + end + + it "deleting non-existing function" do + doc = ArangoDB.log_delete("#{prefix}-delete", api + "/mytest%3Amynonfunc") + + doc.code.should eq(404) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(404) + doc.parsed_response['errorNum'].should eq(1582) + end + end + +################################################################################ +## adding and deleting functions +################################################################################ + + context "adding functions" do + before do + ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest") + end + + after do + ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest") + end + + it "add function, valid code" do + body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-function1", api, :body => body) + + doc.code.should eq(201) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(false) + doc.parsed_response['code'].should eq(201) + end + + it "add function, update" do + body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-function2", api, :body => body) + + doc.code.should eq(201) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(false) + doc.parsed_response['code'].should eq(201) + + doc = ArangoDB.log_post("#{prefix}-add-function2", api, :body => body) + doc.code.should eq(200) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(false) + doc.parsed_response['code'].should eq(200) + end + + it "add function, delete" do + body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-add-function3", api, :body => body) + + doc.code.should eq(201) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(false) + doc.parsed_response['code'].should eq(201) + + doc = ArangoDB.log_delete("#{prefix}-add-function3", api + "/UnitTests%3Amytest") + doc.code.should eq(200) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(false) + doc.parsed_response['code'].should eq(200) + + doc = ArangoDB.log_delete("#{prefix}-add-function3", api + "/UnitTests%3Amytest") + doc.code.should eq(404) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(404) + doc.parsed_response['errorNum'].should eq(1582) + end + end + +################################################################################ +## retrieving the list of functions +################################################################################ + + context "adding functions" do + before do + ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest") + end + + after do + ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest") + end + + it "add function and retrieve the list" do + body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }" + doc = ArangoDB.log_post("#{prefix}-list-functions", api, :body => body) + + doc.code.should eq(201) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(false) + doc.parsed_response['code'].should eq(201) + + doc = ArangoDB.log_get("#{prefix}-list-functions", api + "?prefix=UnitTests") + doc.code.should eq(200) + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response.length.should eq(1) + doc.parsed_response[0]['name'].should eq("UnitTests:mytest") + + end + + end + + end +end + diff --git a/UnitTests/HttpInterface/run-tests b/UnitTests/HttpInterface/run-tests index cab8cae8d4..c6fb585e97 100755 --- a/UnitTests/HttpInterface/run-tests +++ b/UnitTests/HttpInterface/run-tests @@ -4,6 +4,7 @@ test -d logs || mkdir logs rspec --color --format d \ api-http-spec.rb \ api-admin-spec.rb \ + api-aqlfunction-spec.rb \ api-attributes-spec.rb \ api-batch-spec.rb \ api-collection-spec.rb \ diff --git a/UnitTests/Makefile.unittests b/UnitTests/Makefile.unittests index 78f15c25f2..83515b4e42 100755 --- a/UnitTests/Makefile.unittests +++ b/UnitTests/Makefile.unittests @@ -134,6 +134,15 @@ start-server: unittests-make: @(ctags --version > /dev/null 2> /dev/null && make tags > /dev/null || test "x$(FORCE)" == "x1") || true +################################################################################ +### @brief static codebase tests (a.k.a. Visual Studio cries) +################################################################################ + +unittests-codebase-static: + @rm -f duplicates + @(find lib arangosh arangod arangoirb -regex ".*/.*\.\(c\|h\|cpp\)" -printf "%f\n" | sort | uniq -c | grep -v "^ \+1 \+" > duplicates) || true + @if [ "`grep " " duplicates`" != "" ]; then echo ; echo "Duplicate filenames found. These should be fixed to allow compilation with Visual Studio:"; cat duplicates; false; fi + ################################################################################ ### @brief BOOST TESTS ################################################################################ diff --git a/js/actions/api-aqlfunction.js b/js/actions/api-aqlfunction.js new file mode 100644 index 0000000000..6c457b740c --- /dev/null +++ b/js/actions/api-aqlfunction.js @@ -0,0 +1,235 @@ +//////////////////////////////////////////////////////////////////////////////// +/// @brief AQL user functions management +/// +/// @file +/// +/// DISCLAIMER +/// +/// Copyright 2012 triagens GmbH, Cologne, Germany +/// +/// 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. +/// +/// Copyright holder is triAGENS GmbH, Cologne, Germany +/// +/// @author Jan Steemann +/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + +var arangodb = require("org/arangodb"); +var actions = require("org/arangodb/actions"); +var aqlfunctions = require("org/arangodb/aql/functions"); + +// ----------------------------------------------------------------------------- +// --SECTION-- private functions +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup ArangoAPI +/// @{ +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +/// @brief fetch a user +/// +/// @RESTHEADER{GET /_api/aqlfunction,returns registered AQL user functions} +/// +/// @REST{GET /_api/aqlfunction} +/// +/// Returns all registered AQL user functions. +/// +/// @REST{GET /_api/aqlfunction?namespace=@FA{namespace}} +/// +/// Returns all registered AQL user functions from namespace @FA{namespace}. +/// +/// The call will return a JSON list with all user functions found. Each user +/// function will at least have the following attributes: +/// +/// - @LIT{name}: The fully qualified name of the user function +/// +/// - @LIT{code}: A string representation of the function body +//////////////////////////////////////////////////////////////////////////////// + +function GET_api_aqlfunction (req, res) { + if (req.suffix.length != 0) { + actions.resultBad(req, res, arangodb.ERROR_HTTP_BAD_PARAMETER); + return; + } + + var namespace = undefined; + if (req.parameters.hasOwnProperty('namespace')) { + namespace = req.parameters.namespace; + } + + var result = aqlfunctions.toArray(namespace); + actions.resultOk(req, res, actions.HTTP_OK, result) +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief create a new AQL user function +/// +/// @RESTHEADER{POST /_api/aqlfunction,creates or replaces an AQL user function} +/// +/// @REST{POST /_api/aqlfunction} +/// +/// The following data need to be passed in a JSON representation in the body of +/// the POST request: +/// +/// - @LIT{name}: the fully qualified name of the user functions. +/// +/// - @LIT{code}: a string representation of the function body. +/// +/// If the function can be registered by the server, the server will respond with +/// @LIT{HTTP 201}. If the function already existed and was replaced by the +/// call, the server will respond with @LIT{HTTP 200}. +/// +/// In case of success, the returned JSON object has the following properties: +/// +/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{false} +/// in this case) +/// +/// - @LIT{code}: the HTTP status code +/// +/// If the JSON representation is malformed or mandatory data is missing from the +/// request, the server will respond with @LIT{HTTP 400}. +/// +/// The body of the response will contain a JSON object with additional error +/// details. The object has the following attributes: +/// +/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{true} in this case) +/// +/// - @LIT{code}: the HTTP status code +/// +/// - @LIT{errorNum}: the server error number +/// +/// - @LIT{errorMessage}: a descriptive error message +/// +/// @EXAMPLES +/// +/// @verbinclude api-aqlfunction-create +//////////////////////////////////////////////////////////////////////////////// + +function POST_api_aqlfunction (req, res) { + var json = actions.getJsonBody(req, res, actions.HTTP_BAD); + + if (json === undefined) { + return; + } + + var result = aqlfunctions.register(json.name, json.code); + + actions.resultOk(req, res, result ? actions.HTTP_OK : actions.HTTP_CREATED, { }); +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief remove an existing AQL user function +/// +/// @RESTHEADER{DELETE /_api/aqlfunction,remove an existing AQL user function} +/// +/// @REST{DELETE /_api/aqlfunction/@FA{name}} +/// +/// Removes an existing AQL user function, identified by @FA{name}. +/// The function name provided in @FA{name} must be fully qualified, including +/// any namespaces. +/// +/// If the function can be removed by the server, the server will respond with +/// @LIT{HTTP 200}. +/// +/// In case of success, the returned JSON object has the following properties: +/// +/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{false} +/// in this case) +/// +/// - @LIT{code}: the HTTP status code +/// +/// If the JSON representation is malformed or mandatory data is missing from the +/// request, the server will respond with @LIT{HTTP 400}. If the specified user +/// does not exist, the server will respond with @LIT{HTTP 404}. +/// +/// The body of the response will contain a JSON object with additional error +/// details. The object has the following attributes: +/// +/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{true} in this case) +/// +/// - @LIT{code}: the HTTP status code +/// +/// - @LIT{errorNum}: the server error number +/// +/// - @LIT{errorMessage}: a descriptive error message +//////////////////////////////////////////////////////////////////////////////// + +function DELETE_api_aqlfunction (req, res) { + if (req.suffix.length !== 1) { + actions.resultBad(req, res, arangodb.ERROR_HTTP_BAD_PARAMETER); + return; + } + + var name = decodeURIComponent(req.suffix[0]); + try { + aqlfunctions.unregister(name); + actions.resultOk(req, res, actions.HTTP_OK, { }); + } + catch (err) { + if (err.errorNum === arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.code) { + actions.resultNotFound(req, res, arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.code); + } + else { + throw err; + } + } +} + +// ----------------------------------------------------------------------------- +// --SECTION-- initialiser +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @brief gateway +//////////////////////////////////////////////////////////////////////////////// + +actions.defineHttp({ + url : "_api/aqlfunction", + context : "api", + + callback : function (req, res) { + try { + switch (req.requestType) { + case actions.GET: + GET_api_aqlfunction(req, res); + break; + + case actions.POST: + POST_api_aqlfunction(req, res); + break; + + case actions.DELETE: + DELETE_api_aqlfunction(req, res); + break; + + default: + actions.resultUnsupported(req, res); + } + } + catch (err) { + actions.resultException(req, res, err); + } + } +}); + +//////////////////////////////////////////////////////////////////////////////// +/// @} +//////////////////////////////////////////////////////////////////////////////// + +// Local Variables: +// mode: outline-minor +// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)" +// End: diff --git a/js/actions/api-user.js b/js/actions/api-user.js index 0f317efb68..ae663222b2 100644 --- a/js/actions/api-user.js +++ b/js/actions/api-user.js @@ -21,7 +21,6 @@ /// /// Copyright holder is triAGENS GmbH, Cologne, Germany /// -/// @author Achim Brandt /// @author Jan Steemann /// @author Copyright 2012, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// @@ -316,9 +315,8 @@ function PATCH_api_user (req, res) { /// /// - @LIT{code}: the HTTP status code /// -/// If the JSON representation is malformed or mandatory data is missing from the -/// request, the server will respond with @LIT{HTTP 400}. If the specified user -/// does not exist, the server will respond with @LIT{HTTP 404}. +/// If the specified user does not exist, the server will respond with +/// @LIT{HTTP 404}. /// /// The body of the response will contain a JSON object with additional error /// details. The object has the following attributes: diff --git a/js/common/bootstrap/module-internal.js b/js/common/bootstrap/module-internal.js index ba3d9b2083..9b1a3c7070 100644 --- a/js/common/bootstrap/module-internal.js +++ b/js/common/bootstrap/module-internal.js @@ -158,13 +158,13 @@ } //////////////////////////////////////////////////////////////////////////////// -/// @brief plattform +/// @brief platform //////////////////////////////////////////////////////////////////////////////// - exports.plattform = "unknown"; + exports.platform = "unknown"; if (typeof SYS_PLATFORM !== "undefined") { - exports.plattform = SYS_PLATFORM; + exports.platform = SYS_PLATFORM; delete SYS_PLATFORM; } diff --git a/js/common/modules/org/arangodb/aql/functions.js b/js/common/modules/org/arangodb/aql/functions.js index 3e596a5003..a65e0441b8 100644 --- a/js/common/modules/org/arangodb/aql/functions.js +++ b/js/common/modules/org/arangodb/aql/functions.js @@ -47,6 +47,33 @@ var ArangoError = arangodb.ArangoError; /// @{ //////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +/// @brief apply a prefix filter on the functions +//////////////////////////////////////////////////////////////////////////////// + +var getFiltered = function (group) { + var result = [ ]; + + if (group !== null && group !== undefined && group.length > 0) { + var prefix = group.toUpperCase(); + + if (group.substr(group.length - 1, 1) !== ':') { + prefix += ':'; + } + + getStorage().toArray().forEach(function (f) { + if (f.name.toUpperCase().substr(0, prefix.length) === prefix) { + result.push(f); + } + }); + } + else { + result = getStorage().toArray(); + } + + return result; +}; + //////////////////////////////////////////////////////////////////////////////// /// @brief validate a function name //////////////////////////////////////////////////////////////////////////////// @@ -109,7 +136,7 @@ var getStorage = function () { if (functions === null) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_ARANGO_COLLECTION_NOT_FOUND.code; - err.errorMessage = "collection _aqlfunctions not found"; + err.errorMessage = "collection '_aqlfunctions' not found"; throw err; } @@ -131,7 +158,22 @@ var getStorage = function () { //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// +/// @fn JSF_aqlfunctions_unregister /// @brief delete an existing AQL user function +/// +/// @FUN{aqlfunctions.unregister(@FA{name})} +/// +/// Unregisters an existing AQL user function, identified by the fully qualified +/// function name. +/// +/// Trying to unregister a function that does not exist will result in an +/// exception. +/// +/// @EXAMPLES +/// +/// @code +/// arangosh> require("org/arangodb/aql/functions").unregister("myfunctions:temperature:celsiustofahrenheit"); +/// @endcode //////////////////////////////////////////////////////////////////////////////// var unregisterFunction = function (name) { @@ -148,7 +190,7 @@ var unregisterFunction = function (name) { if (func === null) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.code; - err.errorMessage = arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.message; + err.errorMessage = internal.sprintf(arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.message, name); throw err; } @@ -160,16 +202,79 @@ var unregisterFunction = function (name) { }; //////////////////////////////////////////////////////////////////////////////// +/// @fn JSF_aqlfunctions_unregister_group +/// @brief delete a group of AQL user functions +/// +/// @FUN{aqlfunctions.unregisterGroup(@FA{prefix})} +/// +/// Unregisters a group of AQL user function, identified by a common function +/// group prefix. +/// +/// This will return the number of functions unregistered. +/// +/// @EXAMPLES +/// +/// @code +/// arangosh> require("org/arangodb/aql/functions").unregisterGroup("myfunctions:temperature"); +/// +/// arangosh> require("org/arangodb/aql/functions").unregisterGroup("myfunctions"); +/// @endcode +//////////////////////////////////////////////////////////////////////////////// + +var unregisterFunctionsGroup = function (group) { + if (group.length === 0) { + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message; + } + + var deleted = 0; + + getFiltered(group).forEach(function (f) { + getStorage().remove(f._id); + deleted++; + }); + + if (deleted > 0) { + internal.reloadAqlFunctions(); + } + + return deleted; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @fn JSF_aqlfunctions_register /// @brief register an AQL user function +/// +/// @FUN{aqlfunctions.register(@FA{name}, @FA{code})} +/// +/// Registers an AQL user function, identified by a fully qualified function +/// name. The function code in @FA{code} must be specified as a Javascript +/// function or a string representation of a Javascript function. +/// +/// If a function identified by @FA{name} already exists, the previous function +/// definition will be updated. +/// +/// @EXAMPLES +/// +/// @code +/// arangosh> require("org/arangodb/aql/functions").register("myfunctions:temperature:celsiustofahrenheit", +/// function (celsius) { +/// return celsius * 1.8 + 32; +/// }); +/// @endcode //////////////////////////////////////////////////////////////////////////////// var registerFunction = function (name, code, isDeterministic) { // validate input validateName(name); code = stringifyFunction(code, name); + + var exists = false; try { unregisterFunction(name); + exists = true; } catch (err) { } @@ -184,15 +289,52 @@ var registerFunction = function (name, code, isDeterministic) { getStorage().save(data); internal.reloadAqlFunctions(); - return true; + return exists; }; //////////////////////////////////////////////////////////////////////////////// -/// @brief reloads the AQL user functons +/// @fn JSF_aqlfunctions_toArray +/// @brief list all AQL user functions +/// +/// @FUN{aqlfunctions.toArray()} +/// +/// Returns all previously registered AQL user functions, with their fully +/// qualified names and function code. +/// +/// The result may optionally be restricted to a specified group of functions +/// by specifying a group prefix: +/// +/// @FUN{aqlfunctions.toArray(@FA{prefix})} +/// +/// @EXAMPLES +/// +/// To list all available user functions: +/// +/// @code +/// arangosh> require("org/arangodb/aql/functions").toArray(); +/// @endcode +/// +/// To list all available user functions in the `myfunctions` namespace: +/// +/// @code +/// arangosh> require("org/arangodb/aql/functions").toArray("myfunctions"); +/// @endcode +/// +/// To list all available user functions in the `myfunctions:temperature` namespace: +/// +/// @code +/// arangosh> require("org/arangodb/aql/functions").toArray("myfunctions:temperature"); +/// @endcode //////////////////////////////////////////////////////////////////////////////// + +var toArrayFunctions = function (group) { + var result = [ ]; -var reloadFunctions = function () { - throw "cannot use abstract reload function"; + getFiltered(group).forEach(function (f) { + result.push({ name: f.name, code: f.code }); + }); + + return result; }; //////////////////////////////////////////////////////////////////////////////// @@ -208,9 +350,10 @@ var reloadFunctions = function () { /// @{ //////////////////////////////////////////////////////////////////////////////// -exports.register = registerFunction; -exports.unregister = unregisterFunction; -exports.reload = reloadFunctions; +exports.unregister = unregisterFunction; +exports.unregisterGroup = unregisterFunctionsGroup; +exports.register = registerFunction; +exports.toArray = toArrayFunctions; //////////////////////////////////////////////////////////////////////////////// /// @} diff --git a/js/common/modules/org/arangodb/users-common.js b/js/common/modules/org/arangodb/users-common.js index e943812337..2ad5a69d8d 100644 --- a/js/common/modules/org/arangodb/users-common.js +++ b/js/common/modules/org/arangodb/users-common.js @@ -158,8 +158,8 @@ var getStorage = function () { /// @EXAMPLES /// /// @code -/// arangosh> require("users").save("my-user", "my-secret-password"); -/// arangosh> require("users").reload(); +/// arangosh> require("org/arangodb/users").save("my-user", "my-secret-password"); +/// arangosh> require("org/arangodb/users").reload(); /// @endcode //////////////////////////////////////////////////////////////////////////////// @@ -231,8 +231,8 @@ exports.save = function (username, passwd, active, extra) { /// @EXAMPLES /// /// @code -/// arangosh> require("users").replace("my-user", "my-changed-password"); -/// arangosh> require("users").reload(); +/// arangosh> require("org/arangodb/users").replace("my-user", "my-changed-password"); +/// arangosh> require("org/arangodb/users").reload(); /// @endcode //////////////////////////////////////////////////////////////////////////////// @@ -304,8 +304,8 @@ exports.replace = function (username, passwd, active, extra) { /// @EXAMPLES /// /// @code -/// arangosh> require("users").replace("my-user", "my-secret-password"); -/// arangosh> require("users").reload(); +/// arangosh> require("org/arangodb/users").replace("my-user", "my-secret-password"); +/// arangosh> require("org/arangodb/users").reload(); /// @endcode //////////////////////////////////////////////////////////////////////////////// @@ -364,8 +364,8 @@ exports.update = function (username, passwd, active, extra) { /// @EXAMPLES /// /// @code -/// arangosh> require("users").remove("my-user"); -/// arangosh> require("users").reload(); +/// arangosh> require("org/arangodb/users").remove("my-user"); +/// arangosh> require("org/arangodb/users").reload(); /// @endcode //////////////////////////////////////////////////////////////////////////////// diff --git a/js/common/tests/shell-aqlfunctions.js b/js/common/tests/shell-aqlfunctions.js index fcb43f42f9..73a1aa6722 100644 --- a/js/common/tests/shell-aqlfunctions.js +++ b/js/common/tests/shell-aqlfunctions.js @@ -60,6 +60,7 @@ function AqlFunctionsSuite () { //////////////////////////////////////////////////////////////////////////////// setUp : function () { + aqlfunctions.unregisterGroup("UnitTests:"); }, //////////////////////////////////////////////////////////////////////////////// @@ -67,6 +68,43 @@ function AqlFunctionsSuite () { //////////////////////////////////////////////////////////////////////////////// tearDown : function () { + aqlfunctions.unregisterGroup("UnitTests:"); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief toArray +//////////////////////////////////////////////////////////////////////////////// + + testToArray1 : function () { + aqlfunctions.register("UnitTests:tryme:foo", function (what) { return what * 2; }, true); + aqlfunctions.register("UnitTests:tryme:bar", function (what) { return what * 2; }, true); + + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:tryme").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:tryme:").sort().map(function (f) { return f.name; })); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief toArray +//////////////////////////////////////////////////////////////////////////////// + + testToArray2 : function () { + aqlfunctions.register("UnitTests:tryme:foo", function (what) { return what * 2; }, true); + aqlfunctions.register("UnitTests:tryme:bar", function (what) { return what * 2; }, true); + aqlfunctions.register("UnitTests58:tryme:bar", function (what) { return what * 2; }, true); + aqlfunctions.register("UnitTests58:whyme:bar", function (what) { return what * 2; }, true); + + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:tryme").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests58:tryme:bar", "UnitTests58:whyme:bar" ], aqlfunctions.toArray("UnitTests58").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests58:tryme:bar", "UnitTests58:whyme:bar" ], aqlfunctions.toArray("UnitTests58:").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests58:tryme:bar" ], aqlfunctions.toArray("UnitTests58:tryme").sort().map(function (f) { return f.name; })); + assertEqual([ "UnitTests58:tryme:bar" ], aqlfunctions.toArray("UnitTests58:tryme:").sort().map(function (f) { return f.name; })); + + aqlfunctions.unregister("UnitTests58:tryme:bar", function (what) { return what * 2; }, true); + aqlfunctions.unregister("UnitTests58:whyme:bar", function (what) { return what * 2; }, true); }, //////////////////////////////////////////////////////////////////////////////// diff --git a/js/common/tests/shell-fs.js b/js/common/tests/shell-fs.js index 43174599d9..85e59e7f4e 100644 --- a/js/common/tests/shell-fs.js +++ b/js/common/tests/shell-fs.js @@ -49,9 +49,14 @@ function FileSystemSuite () { //////////////////////////////////////////////////////////////////////////////// setUp : function () { + // create a name for a temporary directory we will run all tests in + // as we are creating a new name, this shouldn't collide with any existing + // directories, and we can also remove our directory when the tests are + // over tempDir = fs.join(fs.getTempFile('', false)); try { + // create this directory before any tests fs.makeDirectoryRecursive(tempDir); } catch (err) { @@ -63,38 +68,49 @@ function FileSystemSuite () { //////////////////////////////////////////////////////////////////////////////// tearDown : function () { + // some sanity check as we don't want to unintentionally remove "." or "/" if (tempDir.length > 5) { + // remove our temporary directory with all its subdirectories + // we created it, so we don't care what's in it fs.removeDirectoryRecursive(tempDir); } }, //////////////////////////////////////////////////////////////////////////////// -/// @brief exists() +/// @brief exists() - test if a file / directory exists //////////////////////////////////////////////////////////////////////////////// testExists : function () { var tempName; - // existing directory + // create the name for a new directory tempName = fs.join(tempDir, 'foo'); try { + // remove it if it exists fs.removeDirectory(tempName); } catch (err) { } + // create the directory with all paths to it fs.makeDirectoryRecursive(tempName); + // now the directory should exist assertTrue(fs.exists(tempName)); + + // remove it again fs.removeDirectory(tempName); // non-existing directory/file tempName = fs.join(tempDir, 'meow'); + // this file should not exist assertFalse(fs.exists(tempName)); - // file + // create a new file tempName = fs.getTempFile('', true); + // the file should now exist assertTrue(fs.exists(tempName)); + // clean it up fs.remove(tempName); }, @@ -105,30 +121,40 @@ function FileSystemSuite () { testGetTempFile : function () { var tempName; - // creating a new file + // creating a new empty file with a temporary name tempName = fs.getTempFile('', true); assertTrue(tempName !== ''); + // the file should be located inside the tempPath assertEqual(fs.getTempPath(), tempName.substr(0, fs.getTempPath().length)); + // the file should exist assertTrue(fs.exists(tempName)); + + // clean up fs.remove(tempName); - // without creating + // create a filename only, without creating the file itself tempName = fs.getTempFile('', false); assertTrue(tempName !== ''); + // filename should be located underneath tempPath assertEqual(fs.getTempPath(), tempName.substr(0, fs.getTempPath().length)); + // the file should not exist assertFalse(fs.exists(tempName)); - // in a subdirectory + // create a temporary filename for a file in a subdirectory, without creating the file tempName = fs.getTempFile('tests', false); assertTrue(tempName !== ''); + // filename should be located underneath tempPath assertEqual(fs.join(fs.getTempPath(), 'test'), tempName.substr(0, fs.join(fs.getTempPath(), 'test').length)); + // file should not yet exist assertFalse(fs.exists(tempName)); + + // clean up fs.removeDirectory(fs.join(fs.getTempPath(), 'tests')); }, //////////////////////////////////////////////////////////////////////////////// -/// @brief getTempPath() +/// @brief getTempPath() - test the system's temp path //////////////////////////////////////////////////////////////////////////////// testGetTempPath : function () { @@ -137,76 +163,102 @@ function FileSystemSuite () { tempName = fs.getTempPath(); assertTrue(tempName !== ''); + // the temp path should also be an existing directory assertTrue(fs.isDirectory(tempName)); }, //////////////////////////////////////////////////////////////////////////////// -/// @brief isDirectory() +/// @brief isDirectory() - test if a directory exists //////////////////////////////////////////////////////////////////////////////// testIsDirectory : function () { var tempName; - // existing directory + // create a new directory name tempName = fs.join(tempDir, 'foo'); try { + // remove any existing directory first fs.removeDirectory(tempName); } catch (err) { } + // create the directory with the full path to it fs.makeDirectoryRecursive(tempName); + // check if it does exist assertTrue(fs.exists(tempName)); + // should also be a directory assertTrue(fs.isDirectory(tempName)); + // remove the directory fs.removeDirectory(tempName); // non-existing directory/file tempName = fs.join(tempDir, 'meow'); + // this file shouldn't exist assertFalse(fs.exists(tempName)); + // and thus shouldn't be a directory assertFalse(fs.isDirectory(tempName)); - // file + // create a new file tempName = fs.getTempFile('', true); + // should exist assertTrue(fs.exists(tempName)); + // but shouldn't be a directory assertFalse(fs.isDirectory(tempName)); + + // clean up fs.remove(tempName); }, //////////////////////////////////////////////////////////////////////////////// -/// @brief isFile() +/// @brief isFile() - test if a file exists //////////////////////////////////////////////////////////////////////////////// testIsFile : function () { var tempName; - // existing directory + // create a new directory name tempName = fs.join(tempDir, 'foo'); try { + // remove any existing directory if it exists fs.removeDirectory(tempName); } catch (err) { } + try { + // remove any existing file if it exists fs.remove(tempName); } catch (err) { } + // now create the whole directory with all paths to it fs.makeDirectoryRecursive(tempName); + + // directory should now exist assertTrue(fs.exists(tempName)); + // but should not be a file assertFalse(fs.isFile(tempName)); + // remove it fs.removeDirectory(tempName); // non-existing directory/file tempName = fs.join(tempDir, 'meow'); + // this file shouldn't exist assertFalse(fs.exists(tempName)); + // and shouldn't be a file assertFalse(fs.isFile(tempName)); - // file + // now create a new file tempName = fs.getTempFile('', true); + // should exist assertTrue(fs.exists(tempName)); + // and should be a file assertTrue(fs.isFile(tempName)); + + // clean up and remove the file fs.remove(tempName); }, @@ -218,37 +270,53 @@ function FileSystemSuite () { }, //////////////////////////////////////////////////////////////////////////////// -/// @brief makeDirectory() +/// @brief makeDirectory() - create a directory //////////////////////////////////////////////////////////////////////////////// testMakeDirectory : function () { var tempName; + // create the name for a subdirectory tempName = fs.join(tempDir, 'bar'); try { + // remove an existing directory if it already exists fs.removeDirectoryRecursive(tempName); } catch (err) { } + // the directory shouldn't exist. we just deleted it if it was there assertFalse(fs.exists(tempName)); + + // now create the directory fs.makeDirectory(tempName); + + // directory should be there after creation assertTrue(fs.exists(tempName)); + + // now remove it fs.removeDirectory(tempName); + // create the name for another subdirectory tempName = fs.join(tempDir, 'baz'); try { + // remove it if it does exist fs.removeDirectoryRecursive(tempName); } catch (err) { } - // make with an existing directory + // the directory shouldn't be there... assertFalse(fs.exists(tempName)); + + // now create it fs.makeDirectory(tempName); + + // should have succeeded assertTrue(fs.exists(tempName)); try { + // try to create the same directory again. this should fail fs.makeDirectory(tempName); fail(); } @@ -256,16 +324,21 @@ function FileSystemSuite () { assertEqual(ERRORS.ERROR_SYS_ERROR.code, err.errorNum); } - // make subdirectory + // create a subdirectory in the directory we created tempName = fs.join(tempDir, 'baz', 'foo'); fs.makeDirectory(tempName); + // the subdirectory should now be there assertTrue(fs.exists(tempName)); + // create a file in the subdirecory tempName = fs.join(tempDir, 'baz', 'foo', 'test'); + // write something to the file fs.write(tempName, "this is a test"); + // the file should exist after writing to it assertTrue(fs.exists(tempName)); try { + // create a directory with the name of an already existing file. this should fail. fs.makeDirectory(tempName); fail(); } @@ -273,73 +346,83 @@ function FileSystemSuite () { assertEqual(ERRORS.ERROR_SYS_ERROR.code, err.errorNum); } + // remove all stuff we created for testing fs.removeDirectoryRecursive(fs.join(tempDir, 'baz')); }, //////////////////////////////////////////////////////////////////////////////// -/// @brief makeDirectoryRecursive() +/// @brief makeDirectoryRecursive() - create a directory will all paths to it //////////////////////////////////////////////////////////////////////////////// testMakeDirectoryRecursive : function () { var tempName; + // create the name for a new subdirectory tempName = fs.join(tempDir, 'bar'); try { + // make sure it does not yet exist fs.removeDirectoryRecursive(tempName); } catch (err) { } - // create + // create the subdirectory fs.makeDirectoryRecursive(tempName); + // check if it is there assertTrue(fs.isDirectory(tempName)); - // create again + // create the subdirectory again. this should not fail fs.makeDirectoryRecursive(tempName); + // should still be a directory assertTrue(fs.isDirectory(tempName)); - // create subdirectories + // create subdirectory in subdirectory of subdirectory tempName = fs.join(tempDir, 'bar', 'baz', 'test'); fs.makeDirectoryRecursive(tempName); assertTrue(fs.isDirectory(tempName)); }, //////////////////////////////////////////////////////////////////////////////// -/// @brief move() +/// @brief move() - TODO //////////////////////////////////////////////////////////////////////////////// testMove : function () { }, //////////////////////////////////////////////////////////////////////////////// -/// @brief read() +/// @brief read() - TODO //////////////////////////////////////////////////////////////////////////////// testRead : function () { }, //////////////////////////////////////////////////////////////////////////////// -/// @brief remove() +/// @brief remove() - TODO //////////////////////////////////////////////////////////////////////////////// testRemove : function () { }, //////////////////////////////////////////////////////////////////////////////// -/// @brief size() +/// @brief test size() - the filesize of a file //////////////////////////////////////////////////////////////////////////////// testSize : function () { var tempName; - // existing file + // create a new file with a specific content tempName = fs.join(tempDir, 'foo'); fs.write(tempName, "this is a test file"); + + // test the size of the new file assertEqual(19, fs.size(tempName)); + + // remove the new file fs.remove(tempName); - // non-existing file + // now the file does not exist try { + // try to read filesize. this should fail fs.size(tempName); fail(); } @@ -350,12 +433,15 @@ function FileSystemSuite () { // directory fs.makeDirectory(tempName); try { + // try to read the filesize of a directory. this should fail fs.size(tempName); fail(); } catch (err) { assertEqual(ERRORS.ERROR_FILE_NOT_FOUND.code, err.errorNum); } + + // remove the directory fs.removeDirectory(tempName); }