234 attempt to fix js error Uncaught SyntaxError: Unexpected identifier 'hidden'

This commit is contained in:
2026-04-27 17:57:24 +03:00
parent 8a32a0d5f0
commit 74c073bd7e
149 changed files with 14361 additions and 8886 deletions
+1 -1
View File
@@ -57,7 +57,7 @@
//variables.framework.docs={}; //variables.framework.docs={};
variables.framework.docs.APIName="svc-api"; variables.framework.docs.APIName="svc-api";
variables.framework.docs.APIVersion="0.233"; /*$ git config --global --unset user.password*/ variables.framework.docs.APIVersion="0.234"; /*$ git config --global --unset user.password*/
variables.framework.globalHeaders = structNew(); variables.framework.globalHeaders = structNew();
variables.framework.globalHeaders["Access-Control-Expose-Headers"] = "Location"; variables.framework.globalHeaders["Access-Control-Expose-Headers"] = "Location";
+7 -3
View File
@@ -112,11 +112,15 @@
<!--- , modifier_id ---> <!--- , modifier_id --->
limit #maxrows# limit #maxrows#
</cfquery> </cfquery>
<!--- <cfdump var=#local.qRead#/><cfabort/> ---> <!--- <cfdump var=#local.qRead#/><cfabort/> --->
<cfquery name="local.qTotal">
select count(*) as cnt
from service_catalog.service s
</cfquery>
<cfset "out.queryDurationMs"=getTickCount() - request.startTickCount/> <cfset "out.queryDurationMs"=getTickCount() - request.startTickCount/>
<cfset "out.total"=#local.qTotal.cnt#/>
<cfset "out.resultSetSize"=#local.qRead.recordCount#/> <cfset "out.resultSetSize"=#local.qRead.recordCount#/>
<cfset "out.pageSize"=#arguments.pageSize*1#/> <cfset "out.pageSize"=#arguments.pageSize*1#/>
<cfset "out.page"=#arguments.page*1#/> <cfset "out.page"=#arguments.page*1#/>
@@ -124,7 +128,7 @@
<cfset var resultCollection=[]/> <cfset var resultCollection=[]/>
<cfloop query=#local.qRead# startRow=#startrow# endRow=#(startrow+maxrows-1)#> <cfloop query=#local.qRead# startRow=#startrow# endRow=#(startrow+maxrows-1)#>
<cfset var rec={}/> <cfset var rec=structNew("linked")/>
<cfset this.helper.appendRecord(rec, "", local.titleMap, local.qRead, this.helper.snake2camel)/> <cfset this.helper.appendRecord(rec, "", local.titleMap, local.qRead, this.helper.snake2camel)/>
<cfset arrayAppend(resultCollection, rec)/> <cfset arrayAppend(resultCollection, rec)/>
</cfloop> </cfloop>
+8 -4
View File
@@ -141,11 +141,15 @@
<!--- , modifier_id ---> <!--- , modifier_id --->
limit #maxrows# limit #maxrows#
</cfquery> </cfquery>
<!--- <cfdump var=#local.qRead#/><cfabort/> ---> <!--- <cfdump var=#local.qRead#/><cfabort/> --->
<cfquery name="local.qTotal">
select count(*) as cnt
from service_catalog.service_param p
join service_catalog.service s on (p.service_id=s.service_id)
</cfquery>
<cfset "out.queryDurationMs"=getTickCount() - request.startTickCount/> <cfset "out.queryDurationMs"=getTickCount() - request.startTickCount/>
<cfset "out.total"=#local.qTotal.cnt#/>
<cfset "out.resultSetSize"=#local.qRead.recordCount#/> <cfset "out.resultSetSize"=#local.qRead.recordCount#/>
<cfset "out.pageSize"=#arguments.pageSize*1#/> <cfset "out.pageSize"=#arguments.pageSize*1#/>
<cfset "out.page"=#arguments.page*1#/> <cfset "out.page"=#arguments.page*1#/>
@@ -153,7 +157,7 @@
<cfset var resultCollection=[]/> <cfset var resultCollection=[]/>
<cfloop query=#local.qRead# startRow=#startrow# endRow=#(startrow+maxrows-1)#> <cfloop query=#local.qRead# startRow=#startrow# endRow=#(startrow+maxrows-1)#>
<cfset var rec={}/> <cfset var rec=structNew("linked")/>
<cfset this.helper.appendRecord(rec, "", local.titleMap, local.qRead, this.helper.snake2camel)/> <cfset this.helper.appendRecord(rec, "", local.titleMap, local.qRead, this.helper.snake2camel)/>
<cfset arrayAppend(resultCollection, rec)/> <cfset arrayAppend(resultCollection, rec)/>
</cfloop> </cfloop>
-14
View File
@@ -1,14 +0,0 @@
changelog:
exclude:
labels:
- release-ignore
categories:
- title: Breaking Changes ⚠️
labels:
- Semver-Major
- title: New Features 🎉
labels:
- Semver-Minor
- title: Bug Fixes 🐞
labels:
- Semver-Patch
-22
View File
@@ -1,22 +0,0 @@
name: Run tests for all PRs
on:
push:
branches:
- feature/github-actions
jobs:
test:
runs-on: ubuntu-20.04
steps:
- name: Checkout Code
id: git-checkout
uses: actions/checkout@v2
- name: Install dependencies
uses: pixl8/github-action-box-install@v2
- name: Run tests
uses: pixl8/github-action-box-test@v3
with:
output_file: /tests/test-results.txt
-86
View File
@@ -1,86 +0,0 @@
name: Release New Version
# Based on: https://github.blog/2021-12-16-5-automations-every-developer-should-be-running/
on:
workflow_dispatch:
inputs:
version:
description: "Largest semver bump of new version (major / minor / patch)"
required: true
type: choice
options:
- patch
- minor
- major
jobs:
bump-version:
runs-on: ubuntu-latest
steps:
# Check out the content (source branch). Use a deploy key so that
# when we push changes, it will trigger the release workflow
# run that runs on: tag. (Using the GitHub token would
# not run the workflow to prevent infinite recursion.)
- name: Check out source
uses: actions/checkout@v2
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Setup Git
run: |
git config user.name 'release-bot'
git config user.email 'adamtuttlecodes@gmail.com'
- name: bump version
run: npm version ${{ github.event.inputs.version }}
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@master
- name: Update version in api.cfc
run: |
cat core/api.cfc | sed 's/local\._taffy\.version = \"[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\"/local._taffy.version = "${{ steps.package-version.outputs.current-version }}"/gi' > core/api.new.cfc
rm -f core/api.cfc
mv core/api.new.cfc core/api.cfc
git add core/api.cfc
git commit --amend --no-edit
- name: Cut a docs file for the new release
run: |
cat docs/@next.md | sed 's/@version@/${{ steps.package-version.outputs.current-version }}/gi' > docs/${{ steps.package-version.outputs.current-version }}.md
git add docs/${{ steps.package-version.outputs.current-version }}.md
cat docs/readme.md | sed 's/\<\!--new_docs_links_here--\>/\<\!--new_docs_links_here--\>\n\n- \[\v${{ steps.package-version.outputs.current-version }}]\(${{ steps.package-version.outputs.current-version }}.md\)/g' > docs/readme-next.md
rm -f docs/readme.md
mv docs/readme-next.md docs/readme.md
git add docs/readme.md
git commit --amend --no-edit
- name: Push latest version
run: git push origin main --follow-tags
- name: Create Release Notes
uses: actions/github-script@v4.0.2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
await github.request(`POST /repos/${{ github.repository }}/releases`, {
tag_name: "v${{ steps.package-version.outputs.current-version }}",
generate_release_notes: true
});
- name: Tweet-trigger-publish-release
uses: mugi111/tweet-trigger-release@v1.1
with:
consumer_key: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
consumer_secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
access_token_key: ${{ secrets.TWITTER_ACCESS_TOKEN }}
access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
tweet_body: "🍬 Taffy Version ${{steps.package-version.outputs.current-version}} was just released! https://github.com/atuttle/Taffy/releases/tag/v${{ steps.package-version.outputs.current-version }}"
-19
View File
@@ -1,19 +0,0 @@
name: Require Semver Label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, ready_for_review, synchronize]
jobs:
check_pr_labels:
runs-on: ubuntu-latest
name: Verify that the PR has a valid label
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Verify PR label action
uses: jesusvasquez333/verify-pr-label-action@v1.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
valid-labels: 'Semver-Major, Semver-Minor, Semver-Patch'
pull-request-number: '${{ github.event.pull_request.number }}'
+3 -2
View File
@@ -47,5 +47,6 @@ out/
# generated by JIRA plugin # generated by JIRA plugin
atlassian-ide-plugin.xml atlassian-ide-plugin.xml
# generated by Crashlytics plugin (for Android Studio and Intellij) # docs/etc that shouldn't be committed
com_crashlytics_export_strings.xml .ralph
.ignore
-28
View File
@@ -1,28 +0,0 @@
language: java
sudo: false
jdk:
- openjdk8
services:
- mysql
env:
global:
- TEST_RUNNER_URI=/tests/tests/run.cfm?reporter=text
- DB_USER=travis
- DB_PASS=
matrix:
- CFENGINE=lucee@4.5
- CFENGINE=lucee@5
- CFENGINE=adobe@10
- CFENGINE=adobe@11
- CFENGINE=adobe@2016
before_install:
- git clone --depth 1 https://github.com/foundeo/cfmatrix.git cfmatrix
- mysql -e 'CREATE DATABASE IF NOT EXISTS buglog;'
install:
- bash ./cfmatrix/install.sh
- mysql buglog < tests/BugLogHQ/install/mysql.sql
before_script:
- cfmatrix/bin/box cfconfig cfmapping list
script:
- bash ./cfmatrix/run.sh
- cfmatrix/bin/box server log
+40
View File
@@ -0,0 +1,40 @@
# Taffy
REST API framework for CFML
## Running Tests
You can check if box ("commandbox") cli is install by running `box version`.
You can install box by running `brew install commandbox`.
Then install project dependencies: `box install`
### Start servers
box run-script start:all
box run-script start:lucee5
box run-script start:lucee6
box run-script start:lucee7
### Run tests
box run-script test:all
box run-script test:lucee5
box run-script test:lucee6
box run-script test:lucee7
### Stop servers
box run-script stop:all
box run-script stop:lucee5
box run-script stop:lucee6
box run-script stop:lucee7
## Documentation References
- Taffy documentation: `./docs/`
- CFML Syntax docs
- cfdocs has cross-platform reference data in `.ignore/cfdocs/`
- Lucee docs are in `.ignore/lucee-docs/`
- Testbox docs: `.ignore/testbox-docs/`
- Commandbox docs: `.ignore/commandbox-docs/`
+6 -7
View File
@@ -7,7 +7,7 @@
**PROBLEM:** Creating REST APIs with the native functionality in ColdFusion 10+ and Lucee is verbose, complex, and developer-hostile. Or maybe you're still on an older version of ColdFusion. **PROBLEM:** Creating REST APIs with the native functionality in ColdFusion 10+ and Lucee is verbose, complex, and developer-hostile. Or maybe you're still on an older version of ColdFusion.
**SOLUTION:** A framework that focuses on Developer Experience and helps you "fall into a pit of success". **You've come to the right place.** Taffy is low friction, simple to get started, and compatible as far back as ColdFusion 8. CF8 was released in 2007! 😱 **SOLUTION:** A framework that focuses on Developer Experience and helps you "fall into a pit of success". **You've come to the right place.** Taffy is low friction, and simple to get started.
--- ---
@@ -21,8 +21,8 @@
## Currently Supported Versions ## Currently Supported Versions
- Taffy 3.3.x+ - Taffy 4.0.0+
- Taffy 2.2.x - Taffy 3.7.x+
If you file a bug or ask for support please indicate which version of Taffy you're using. If it's an older release, we usually ask you to upgrade. Officially, we promise to support the current and previous **minor** releases and the last **minor** release of the previous **major** release. To the best of our abilities Taffy follows the versioning guidelines defined in [semver](https://semver.org/). If you file a bug or ask for support please indicate which version of Taffy you're using. If it's an older release, we usually ask you to upgrade. Officially, we promise to support the current and previous **minor** releases and the last **minor** release of the previous **major** release. To the best of our abilities Taffy follows the versioning guidelines defined in [semver](https://semver.org/).
@@ -44,7 +44,7 @@ Features and bug fixes are coordinated via the [GitHub issues list](https://gith
## Community ## Community
The most active place where Taffy users and contributors gather is in the **#taffy** channel of the [CFML Slack][4]. It's a great place to ask for help. We also have [a mailing list][1], but it hasn't been used much since the CFML Slack came around. The most active place where Taffy users and contributors gather is in the **#taffy** channel of the [CFML Slack][4]. It's a great place to ask for help.
## Need serious help? ## Need serious help?
@@ -62,7 +62,6 @@ If your problem is too big or too private to ask for help in a chat room, [I am
> >
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[1]: https://groups.google.com/forum/#!forum/taffy-users
[3]: https://github.com/atuttle/Taffy/wiki [3]: https://github.com/atuttle/Taffy/wiki
[4]: https://cfml-slack.herokuapp.com [4]: https://cfml-slack.net
[5]: https://twitter.com/adamtuttle [5]: https://bsky.app/profile/adamtuttle.codes
+5 -13
View File
@@ -1,15 +1,7 @@
<cfcomponent extends="taffy.core.baseSerializer"> component extends="taffy.core.baseSerializer" {
<!--- remove because causing tests to fail public function getAsXML() output="false" taffy_mime="application/xml" taffy_default="true" {
<cfset variables.anythingToXml = application.anythingToXml /> return application.anythingToXml.toXml(variables.data);
---> }
<cffunction }
name="getAsXML"
output="false"
taffy:mime="application/xml"
taffy:default="true">
<cfreturn application.anythingToXml.toXml(variables.data) />
</cffunction>
</cfcomponent>
+4 -9
View File
@@ -1,11 +1,6 @@
<cfinterface> interface {
<cffunction name="init" hint="I accept a configuration structure to setup and return myself"> public function init(config, tracker) hint="I accept a configuration structure to setup and return myself";
<cfargument name="config" /> public function saveLog(exception) hint="I log or otherwise notify you of an exception";
<cfargument name="tracker" />
</cffunction>
<cffunction name="saveLog" hint="I log or otherwise notify you of an exception">
<cfargument name="exception" />
</cffunction>
</cfinterface> }
-553
View File
@@ -1,553 +0,0 @@
<!---
Copyright 2009 Nathan Mische
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.
--->
<cfcomponent displayname="JSONUtil" output="false">
<cfset this.deserialzeJSON = deserializeFromJSON />
<cfset this.serializeJSON = serializeToJSON />
<cfset this.deserialize = deserializeFromJSON />
<cfset this.serialize = serializeToJSON />
<cffunction name="init" output="false">
<cfreturn this />
</cffunction>
<cffunction
name="deserializeFromJSON"
access="public"
returntype="any"
output="false"
hint="Converts a JSON (JavaScript Object Notation) string data representation into CFML data, such as a CFML structure or array.">
<cfargument
name="JSONVar"
type="string"
required="true"
hint="A string that contains a valid JSON construct, or variable that represents one." />
<cfargument
name="strictMapping"
type="boolean"
required="false"
default="true"
hint="A Boolean value that specifies whether to convert the JSON strictly, as follows:
<ul>
<li><code>true:</code> (Default) Convert the JSON string to ColdFusion data types that correspond directly to the JSON data types.</li>
<li><code>false:</code> Determine if the JSON string contains representations of ColdFusion queries, and if so, convert them to queries.</li>
</ul>" />
<!--- DECLARE VARIABLES --->
<cfset var ar = ArrayNew(1) />
<cfset var st = StructNew() />
<cfset var dataType = "" />
<cfset var inQuotes = false />
<cfset var startPos = 1 />
<cfset var nestingLevel = 0 />
<cfset var dataSize = 0 />
<cfset var i = 1 />
<cfset var skipIncrement = false />
<cfset var j = 0 />
<cfset var char = "" />
<cfset var dataStr = "" />
<cfset var structVal = "" />
<cfset var structKey = "" />
<cfset var colonPos = "" />
<cfset var qRows = 0 />
<cfset var qCols = "" />
<cfset var qCol = "" />
<cfset var qData = "" />
<cfset var curCharIndex = "" />
<cfset var curChar = "" />
<cfset var result = "" />
<cfset var unescapeVals = "\\,\"",\/,\b,\t,\n,\f,\r" />
<cfset var unescapeToVals = "\,"",/,#Chr(8)#,#Chr(9)#,#Chr(10)#,#Chr(12)#,#Chr(13)#" />
<cfset var unescapeVals2 = '\,",/,b,t,n,f,r' />
<cfset var unescapetoVals2 = '\,",/,#Chr(8)#,#Chr(9)#,#Chr(10)#,#Chr(12)#,#Chr(13)#' />
<cfset var dJSONString = "" />
<cfset var _data = Trim(arguments.JSONVar) />
<!--- NUMBER --->
<cfif IsNumeric(_data)>
<cfreturn Val(_data) />
<!--- NULL --->
<cfelseif _data EQ "null">
<cfreturn "null" />
<!--- BOOLEAN --->
<cfelseif ListFindNoCase("true,false", _data)>
<cfreturn _data />
<!--- EMPTY STRING --->
<cfelseif _data EQ "''" OR _data EQ '""'>
<cfreturn "" />
<!--- STRING --->
<cfelseif ReFind('^"[^\\"]*(?:\\.[^\\"]*)*"$', _data) EQ 1 OR ReFind("^'[^\\']*(?:\\.[^\\']*)*'$", _data) EQ 1>
<cfset _data = mid(_data, 2, Len(_data)-2) />
<!--- If there are any \b, \t, \n, \f, and \r, do extra processing
(required because ReplaceList() won't work with those) --->
<cfif Find("\b", _data) OR Find("\t", _data) OR Find("\n", _data) OR Find("\f", _data) OR Find("\r", _data)>
<cfset curCharIndex = 0 />
<cfset curChar = ""/>
<cfset dJSONString = ArrayNew(1) />
<cfloop condition="true">
<cfset curCharIndex = curCharIndex + 1 />
<cfif curCharIndex GT len(_data)>
<cfbreak />
<cfelse>
<cfset curChar = mid(_data, curCharIndex, 1) />
<cfif curChar EQ "\">
<cfset curCharIndex = curCharIndex + 1 />
<cfset curChar = mid(_data, curCharIndex,1) />
<cfset pos = listFind(unescapeVals2, curChar) />
<cfif pos>
<cfset ArrayAppend(dJSONString,ListGetAt(unescapetoVals2, pos)) />
<cfelse>
<cfset ArrayAppend(dJSONString,"\" & curChar) />
</cfif>
<cfelse>
<cfset ArrayAppend(dJSONString,curChar) />
</cfif>
</cfif>
</cfloop>
<cfreturn ArrayToList(dJSONString,"") />
<cfelse>
<cfreturn ReplaceList(_data, unescapeVals, unescapeToVals) />
</cfif>
<!--- ARRAY, STRUCT, OR QUERY --->
<cfelseif ( Left(_data, 1) EQ "[" AND Right(_data, 1) EQ "]" )
OR ( Left(_data, 1) EQ "{" AND Right(_data, 1) EQ "}" )>
<!--- Store the data type we're dealing with --->
<cfif Left(_data, 1) EQ "[" AND Right(_data, 1) EQ "]">
<cfset dataType = "array" />
<cfelseif ReFindNoCase('^\{"ROWCOUNT":[0-9]+,"COLUMNS":\[("[^"]+",?)+\],"DATA":\{("[^"]+":\[[^]]*\],?)+\}\}$', _data, 0) EQ 1 AND NOT arguments.strictMapping>
<cfset dataType = "queryByColumns" />
<cfelseif ReFindNoCase('^\{"COLUMNS":\[("[^"]+",?)+\],"DATA":\[(\[[^]]*\],?)+\]\}$', _data, 0) EQ 1 AND NOT arguments.strictMapping>
<cfset dataType = "query" />
<cfelse>
<cfset dataType = "struct" />
</cfif>
<!--- Remove the brackets --->
<cfset _data = Trim( Mid(_data, 2, Len(_data)-2) ) />
<!--- Deal with empty array/struct --->
<cfif Len(_data) EQ 0>
<cfif dataType EQ "array">
<cfreturn ar />
<cfelse>
<cfreturn st />
</cfif>
</cfif>
<!--- Loop through the string characters --->
<cfset dataSize = Len(_data) + 1 />
<cfloop condition="#i# LTE #dataSize#">
<cfset skipIncrement = false />
<!--- Save current character --->
<cfset char = Mid(_data, i, 1) />
<!--- If char is a quote, switch the quote status --->
<cfif char EQ '"'>
<cfset inQuotes = NOT inQuotes />
<!--- If char is escape character, skip the next character --->
<cfelseif char EQ "\" AND inQuotes>
<cfset i = i + 2 />
<cfset skipIncrement = true />
<!--- If char is a comma and is not in quotes, or if end of string, deal with data --->
<cfelseif (char EQ "," AND NOT inQuotes AND nestingLevel EQ 0) OR i EQ Len(_data)+1>
<cfset dataStr = Mid(_data, startPos, i-startPos) />
<!--- If data type is array, append data to the array --->
<cfif dataType EQ "array">
<cfset arrayappend( ar, deserializeFromJSON(dataStr, arguments.strictMapping) ) />
<!--- If data type is struct or query or queryByColumns... --->
<cfelseif dataType EQ "struct" OR dataType EQ "query" OR dataType EQ "queryByColumns">
<cfset dataStr = Mid(_data, startPos, i-startPos) />
<cfset colonPos = Find('":', dataStr) />
<cfif colonPos>
<cfset colonPos = colonPos + 1 />
<cfelse>
<cfset colonPos = Find(":", dataStr) />
</cfif>
<cfset structKey = Trim( Mid(dataStr, 1, colonPos-1) ) />
<!--- If needed, remove quotes from keys --->
<cfif Left(structKey, 1) EQ "'" OR Left(structKey, 1) EQ '"'>
<cfset structKey = Mid( structKey, 2, Len(structKey)-2 ) />
</cfif>
<cfset structVal = Mid( dataStr, colonPos+1, Len(dataStr)-colonPos ) />
<!--- If struct, add to the structure --->
<cfif dataType EQ "struct">
<cfset StructInsert( st, structKey, deserializeFromJSON(structVal, arguments.strictMapping) ) />
<!--- If query, build the query --->
<cfelseif dataType EQ "queryByColumns">
<cfif structKey EQ "rowcount">
<cfset qRows = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfelseif structKey EQ "columns">
<cfset qCols = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfset st = QueryNew(ArrayToList(qCols)) />
<cfif qRows>
<cfset QueryAddRow(st, qRows) />
</cfif>
<cfelseif structKey EQ "data">
<cfset qData = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfset ar = StructKeyArray(qData) />
<cfloop from="1" to="#ArrayLen(ar)#" index="j">
<cfloop from="1" to="#st.recordcount#" index="qRows">
<cfset qCol = ar[j] />
<cfset QuerySetCell(st, qCol, qData[qCol][qRows], qRows) />
</cfloop>
</cfloop>
</cfif>
<cfelseif dataType EQ "query">
<cfif structKey EQ "columns">
<cfset qCols = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfset st = QueryNew(ArrayToList(qCols)) />
<cfelseif structKey EQ "data">
<cfset qData = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfloop from="1" to="#ArrayLen(qData)#" index="qRows">
<cfset QueryAddRow(st) />
<cfloop from="1" to="#ArrayLen(qCols)#" index="j">
<cfset qCol = qCols[j] />
<cfset QuerySetCell(st, qCol, qData[qRows][j], qRows) />
</cfloop>
</cfloop>
</cfif>
</cfif>
</cfif>
<cfset startPos = i + 1 />
<!--- If starting a new array or struct, add to nesting level --->
<cfelseif "{[" CONTAINS char AND NOT inQuotes>
<cfset nestingLevel = nestingLevel + 1 />
<!--- If ending an array or struct, subtract from nesting level --->
<cfelseif "]}" CONTAINS char AND NOT inQuotes>
<cfset nestingLevel = nestingLevel - 1 />
</cfif>
<cfif NOT skipIncrement>
<cfset i = i + 1 />
</cfif>
</cfloop>
<!--- Return appropriate value based on data type --->
<cfif dataType EQ "array">
<cfreturn ar />
<cfelse>
<cfreturn st />
</cfif>
<!--- INVALID JSON --->
<cfelse>
<cfthrow message="JSON parsing failure." />
</cfif>
</cffunction>
<cffunction
name="serializeToJSON"
access="public"
returntype="string"
output="false"
hint="Converts ColdFusion data into a JSON (JavaScript Object Notation) representation of the data.">
<cfargument
name="var"
type="any"
required="true"
hint="A ColdFusion data value or variable that represents one." />
<cfargument
name="serializeQueryByColumns"
type="boolean"
required="false"
default="false"
hint="A Boolean value that specifies how to serialize ColdFusion queries.
<ul>
<li><code>false</code>: (Default) Creates an object with two entries: an array of column names and an array of row arrays. This format is required by the HTML format cfgrid tag.</li>
<li><code>true</code>: Creates an object that corresponds to WDDX query format.</li>
</ul>">
<cfargument
name="strictMapping"
type="boolean"
required="false"
default="false"
hint="A Boolean value that specifies whether to convert the ColdFusion data strictly, as follows:
<ul>
<li><code>false:</code> (Default) Convert the ColdFusion data to a JSON string using ColdFusion data types.</li>
<li><code>true:</code> Convert the ColdFusion data to a JSON string using underlying Java/SQL data types.</li>
</ul>" />
<!--- VARIABLE DECLARATION --->
<cfset var jsonString = "" />
<cfset var tempVal = "" />
<cfset var arKeys = "" />
<cfset var colPos = 1 />
<cfset var md = "" />
<cfset var rowDel = "" />
<cfset var colDel = "" />
<cfset var className = "" />
<cfset var i = 1 />
<cfset var column = "" />
<cfset var datakey = "" />
<cfset var recordcountkey = "" />
<cfset var columnlist = "" />
<cfset var columnlistkey = "" />
<cfset var columnJavaTypes = "" />
<cfset var dJSONString = "" />
<cfset var escapeToVals = "\\,\"",\/,\b,\t,\n,\f,\r" />
<cfset var escapeVals = "\,"",/,#Chr(8)#,#Chr(9)#,#Chr(10)#,#Chr(12)#,#Chr(13)#" />
<cfset var _data = arguments.var />
<cfif arguments.strictMapping>
<!--- GET THE CLASS NAME --->
<cfset className = getClassName(_data) />
</cfif>
<!--- TRY STRICT MAPPING --->
<cfif Len(className) AND CompareNoCase(className,"java.lang.String") eq 0>
<cfreturn '"' & ReplaceList(_data, escapeVals, escapeToVals) & '"' />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Boolean") eq 0>
<cfreturn ReplaceList(ToString(_data), 'YES,NO', 'true,false') />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Integer") eq 0>
<cfreturn ToString(_data) />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Long") eq 0>
<cfreturn ToString(_data) />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Float") eq 0>
<cfreturn ToString(_data) />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Double") eq 0>
<cfreturn ToString(_data) />
<!--- BINARY --->
<cfelseif IsBinary(_data)>
<cfthrow message="JSON serialization failure: Unable to serialize binary data to JSON." />
<!--- BOOLEAN --->
<cfelseif IsBoolean(_data) AND NOT IsNumeric(_data)>
<cfreturn ReplaceList(YesNoFormat(_data), 'Yes,No', 'true,false') />
<!--- NUMBER --->
<cfelseif IsNumeric(_data)>
<cfif getClassName(_data) eq "java.lang.String">
<cfreturn Val(_data).toString() />
<cfelse>
<cfreturn _data.toString() />
</cfif>
<!--- DATE --->
<cfelseif IsDate(_data)>
<cfreturn '"#DateFormat(_data, "mmmm, dd yyyy")# #TimeFormat(_data, "HH:mm:ss")#"' />
<!--- STRING --->
<cfelseif IsSimpleValue(_data)>
<cfreturn '"' & ReplaceList(_data, escapeVals, escapeToVals) & '"' />
<!--- RAILO XML --->
<cfelseif StructKeyExists(server,"railo") and IsXML(_data)>
<cfreturn '"' & ReplaceList(ToString(_data), escapeVals, escapeToVals) & '"' />
<!--- CUSTOM FUNCTION --->
<cfelseif IsCustomFunction(_data)>
<cfreturn serializeToJSON( GetMetadata(_data), arguments.serializeQueryByColumns, arguments.strictMapping) />
<!--- OBJECT --->
<cfelseif IsObject(_data)>
<cfreturn "{}" />
<!--- ARRAY --->
<cfelseif IsArray(_data)>
<cfset dJSONString = ArrayNew(1) />
<cfloop from="1" to="#ArrayLen(_data)#" index="i">
<cfset tempVal = serializeToJSON( _data[i], arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfset ArrayAppend(dJSONString,tempVal) />
</cfloop>
<cfreturn "[" & ArrayToList(dJSONString,",") & "]" />
<!--- STRUCT --->
<cfelseif IsStruct(_data)>
<cfset dJSONString = ArrayNew(1) />
<cfset arKeys = StructKeyArray(_data) />
<cfloop from="1" to="#ArrayLen(arKeys)#" index="i">
<cfset tempVal = serializeToJSON(_data[ arKeys[i] ], arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfset ArrayAppend(dJSONString,'"' & arKeys[i] & '":' & tempVal) />
</cfloop>
<cfreturn "{" & ArrayToList(dJSONString,",") & "}" />
<!--- QUERY --->
<cfelseif IsQuery(_data)>
<cfset dJSONString = ArrayNew(1) />
<!--- Add query meta data --->
<cfset recordcountKey = "ROWCOUNT" />
<cfset columnlistKey = "COLUMNS" />
<cfset columnlist = "" />
<cfset dataKey = "DATA" />
<cfset md = GetMetadata(_data) />
<cfset columnJavaTypes = StructNew() />
<cfloop from="1" to="#ArrayLen(md)#" index="column">
<cfset columnlist = ListAppend(columnlist,UCase(md[column].Name),',') />
<cfif StructKeyExists(md[column],"TypeName")>
<cfset columnJavaTypes[md[column].Name] = getJavaType(md[column].TypeName) />
<cfelse>
<cfset columnJavaTypes[md[column].Name] = "" />
</cfif>
</cfloop>
<cfif arguments.serializeQueryByColumns>
<cfset ArrayAppend(dJSONString,'"#recordcountKey#":' & _data.recordcount) />
<cfset ArrayAppend(dJSONString,',"#columnlistKey#":[' & ListQualify(columnlist, '"') & ']') />
<cfset ArrayAppend(dJSONString,',"#dataKey#":{') />
<cfset colDel = "">
<cfloop list="#columnlist#" delimiters="," index="column">
<cfset ArrayAppend(dJSONString,colDel) />
<cfset ArrayAppend(dJSONString,'"#column#":[') />
<cfset rowDel = "">
<cfloop from="1" to="#_data.recordcount#" index="i">
<cfset ArrayAppend(dJSONString,rowDel) />
<cfif (arguments.strictMapping or StructKeyExists(server,"railo")) AND Len(columnJavaTypes[column])>
<cfset tempVal = serializeToJSON( JavaCast(columnJavaTypes[column],_data[column][i]), arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfelse>
<cfset tempVal = serializeToJSON( _data[column][i], arguments.serializeQueryByColumns, arguments.strictMapping ) />
</cfif>
<cfset ArrayAppend(dJSONString,tempVal) />
<cfset rowDel = ",">
</cfloop>
<cfset ArrayAppend(dJSONString,']') />
<cfset colDel = ",">
</cfloop>
<cfset ArrayAppend(dJSONString,'}') />
<cfelse>
<cfset ArrayAppend(dJSONString,'"#columnlistKey#":[' & ListQualify(columnlist, '"') & ']') />
<cfset ArrayAppend(dJSONString,',"#dataKey#":[') />
<cfset rowDel = "">
<cfloop from="1" to="#_data.recordcount#" index="i">
<cfset ArrayAppend(dJSONString,rowDel) />
<cfset ArrayAppend(dJSONString,'[') />
<cfset colDel = "">
<cfloop list="#columnlist#" delimiters="," index="column">
<cfset ArrayAppend(dJSONString,colDel) />
<cfif (arguments.strictMapping or StructKeyExists(server,"railo")) AND Len(columnJavaTypes[column])>
<cfset tempVal = serializeToJSON( JavaCast(columnJavaTypes[column],_data[column][i]), arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfelse>
<cfset tempVal = serializeToJSON( _data[column][i], arguments.serializeQueryByColumns, arguments.strictMapping ) />
</cfif>
<cfset ArrayAppend(dJSONString,tempVal) />
<cfset colDel=","/>
</cfloop>
<cfset ArrayAppend(dJSONString,']') />
<cfset rowDel = "," />
</cfloop>
<cfset ArrayAppend(dJSONString,']') />
</cfif>
<cfreturn "{" & ArrayToList(dJSONString,"") & "}">
<!--- XML --->
<cfelseif IsXML(_data)>
<cfreturn '"' & ReplaceList(ToString(_data), escapeVals, escapeToVals) & '"' />
<!--- UNKNOWN OBJECT TYPE --->
<cfelse>
<cfreturn "{}" />
</cfif>
</cffunction>
<cffunction
name="getJavaType"
access="private"
returntype="string"
output="false"
hint="Maps SQL to Java types. Returns blank string for unhandled SQL types.">
<cfargument
name="sqlType"
type="string"
required="true"
hint="A SQL datatype." />
<cfswitch expression="#arguments.sqlType#">
<cfcase value="bit">
<cfreturn "boolean" />
</cfcase>
<cfcase value="tinyint,smallint,integer">
<cfreturn "int" />
</cfcase>
<cfcase value="bigint">
<cfreturn "long" />
</cfcase>
<cfcase value="real,float">
<cfreturn "float" />
</cfcase>
<cfcase value="double">
<cfreturn "double" />
</cfcase>
<cfcase value="char,varchar,longvarchar">
<cfreturn "string" />
</cfcase>
<cfdefaultcase>
<cfreturn "" />
</cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction
name="getClassName"
access="private"
returntype="string"
output="false"
hint="Returns a variable's underlying java Class name.">
<cfargument
name="data"
type="any"
required="true"
hint="A variable." />
<!--- GET THE CLASS NAME --->
<cftry>
<cfreturn arguments.data.getClass().getName() />
<cfcatch type="any">
<cfreturn "" />
</cfcatch>
</cftry>
</cffunction>
</cfcomponent>
+6 -23
View File
@@ -1,26 +1,9 @@
<cfcomponent extends="taffy.core.baseSerializer"> component extends="taffy.core.baseSerializer" {
<!--- <cfset variables.jsonUtil = application.jsonUtil /> variables.jsonUtil = application.jsonUtil;
<cffunction public function getAsJson() output="false" taffy_mime="application/json" taffy_default="true" {
name="getAsJson" return variables.jsonUtil.serialize(variables.data);
output="false" }
taffy:mime="application/json"
taffy:default="true">
<cfreturn variables.jsonUtil.serialize(variables.data) />
</cffunction> --->
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true">
<cfreturn variables.jsonUtil.serialize(variables.data) />
</cffunction>
<cffunction name="setJSONUtil" output="false"> }
<cfargument name="JSONUtil" required="true" />
<cfset variables.jsonUtil = arguments.JSONUtil />
</cffunction>
</cfcomponent>
+44 -52
View File
@@ -1,62 +1,54 @@
<cfcomponent implements="taffy.bonus.ILogAdapter"> component implements="taffy.bonus.ILogAdapter" {
<cffunction name="init"> public function init(config, tracker="") {
<cfargument name="config" /> if (structKeyExists(arguments, "tracker") && isObject(arguments.tracker)) {
<cfargument name="tracker" /> // used to inject mocking object for testing
variables.blhq = arguments.tracker;
} else {
var svc = "bugLog.client.bugLogService";
if (structKeyExists(arguments.config, "service")) {
svc = arguments.config.service;
}
<!--- <cfdump var="#arguments.config#" abort="true" /> ---> variables.blhq = createObject("component", svc);
<cfif structKeyExists(arguments, "tracker")> variables.blhq.init(argumentCollection=arguments.config);
<!--- used to inject mocking object for testing ---> }
<cfset variables.blhq = arguments.tracker />
<cfelse>
<cfset var svc = "bugLog.client.bugLogService" />
<cfif structKeyExists( arguments.config, "service" )>
<cfset svc = arguments.config.service />
</cfif>
<cfset variables.blhq = createObject("component", svc) /> param name="arguments.config.message" default="Exception trapped in API";
<cfset variables.blhq.init( variables.message = arguments.config.message;
argumentCollection=arguments.config
) />
</cfif>
<cfparam name="arguments.config.message" default="Exception trapped in API" /> return this;
<cfset variables.message = arguments.config.message /> }
<cfreturn this /> public function saveLog(exception) {
</cffunction> var msg = '';
<cffunction name="saveLog"> if (structKeyExists(exception, 'rootcause') && structKeyExists(exception.rootcause, 'cause') && structKeyExists(exception.rootcause.cause, 'message')) {
<cfargument name="exception" /> msg = exception.rootcause.cause.message;
} else if (structKeyExists(exception, 'cause') && structKeyExists(exception.cause, 'message')) {
msg = exception.cause.message;
} else if (structKeyExists(exception, 'message')) {
msg = exception.message;
} else {
msg = variables.message;
}
<cfset var msg = '' /> // You can use addDebugData() in resources to set this value
if (structKeyExists(request, "debugData") and !structKeyExists(exception, "extraInfo")) {
exception.extraInfo = request.debugData;
}
<cfif structKeyExists(exception, 'rootcause') && structKeyExists(exception.rootcause, 'cause') && structKeyExists(exception.rootcause.cause, 'message')> var reqHeaders = getHTTPRequestData().headers;
<cfset msg = exception.rootcause.cause.message /> var reqBody = getHTTPRequestData().content;
<cfelseif structKeyExists(exception, 'cause') && structKeyExists(exception.cause, 'message')> // on input with content-type "application/json" CF seems to expose it as binary data. Here we convert it back to plain text
<cfset msg = exception.cause.message /> if (isBinary(reqBody)) {
<cfelseif structKeyExists(exception, 'message')> reqBody = charsetEncode(reqBody, "UTF-8");
<cfset msg = exception.message /> }
<cfelse> if (isJson(reqBody)) {
<cfset msg = variables.message /> reqBody = deserializeJson(reqBody);
</cfif> }
<!--- You can use addDebugData() in resources to set this value ---> variables.blhq.notifyService(msg, arguments.exception, { request_body: reqBody, request_headers: reqHeaders });
<cfif structKeyExists(request, "debugData") and not structKeyExists(exception, "extraInfo")> }
<cfset exception.extraInfo = request.debugData />
</cfif>
<cfset var reqHeaders = getHTTPRequestData().headers /> }
<cfset var reqBody = getHTTPRequestData().content />
<!--- on input with content-type "application/json" CF seems to expose it as binary data. Here we convert it back to plain text --->
<cfif isBinary(reqBody)>
<cfset reqBody = charsetEncode(reqBody, "UTF-8") />
</cfif>
<cfif isJson(reqBody)>
<cfset reqBody = deserializeJson( reqBody ) />
</cfif>
<cfset variables.blhq.notifyService(msg, arguments.exception, { request_body: reqBody, request_headers: reqHeaders }) />
</cffunction>
</cfcomponent>
+110 -125
View File
@@ -1,146 +1,131 @@
<!--- /**
Adapter Path: taffy.bonus.LogToBugsnag * Adapter Path: taffy.bonus.LogToBugsnag
Configuration Options: (structure) * Configuration Options: (structure)
*
* variables.framework.exceptionLogAdapterConfig = {
* apiKey = "c9d60ae4c7e70c4b6c4ebd3e8056d2b8",
* appVersion = "1.1.3",
* releaseStage = "production"
* };
*
* apiKey: The API Key associated with the project
* appVersion: The version number of the application which generated the error
* releaseStage: The release stage that this error occurred in (e.g "development", "staging" or "production")
*/
component implements="taffy.bonus.ILogAdapter" {
variables.framework.exceptionLogAdapterConfig = { /**
apiKey = "c9d60ae4c7e70c4b6c4ebd3e8056d2b8", * Initializes this logger
appVersion = "1.1.3", */
releaseStage = "production" public function init(config, tracker="") hint="I accept a configuration structure to setup and return myself" {
}; variables.config = {};
structAppend(variables.config, arguments.config, true);
return this;
}
apiKey: The API Key associated with the project /**
appVersion: The version number of the application which generated the error * Logs or notifies the specified exception
releaseStage: The release stage that this error occurred in (e.g "development", "staging" or "production") */
---> public function saveLog(exception) hint="I log or otherwise notify you of an exception" {
<cfcomponent implements="taffy.bonus.ILogAdapter"> var payload = {};
<!---
Initializes this logger
--->
<cffunction name="init" hint="I accept a configuration structure to setup and return myself">
<cfargument name="config" />
<cfargument name="tracker" />
<!--- copy settings into adapter instance data ---> // Add Project API key to payload
<cfset variables.config = structNew() /> payload["apiKey"] = variables.config.apiKey;
<cfset structAppend(variables.config, arguments.config, true) />
<cfreturn this /> // Add notifier info to payload
</cffunction> payload["notifier"] = {};
payload["notifier"]["name"] = "Taffy";
payload["notifier"]["version"] = "1.0";
payload["notifier"]["url"] = "https://github.com/atuttle/Taffy";
<!--- // Add exception details to payload
Logs or notifies the specified exception payload["events"] = [];
---> payload["events"][1] = {};
<cffunction name="saveLog" hint="I log or otherwise notify you of an exception"> payload["events"][1]["payloadVersion"] = "2";
<cfargument name="exception" /> payload["events"][1]["exceptions"] = [];
payload["events"][1]["exceptions"][1] = convertException(arguments.exception);
<!--- Define local variables ---> // Add application details to payload
<cfset var payload = structNew() /> payload["events"][1]["app"] = {};
payload["events"][1]["app"]["appVersion"] = variables.config.appVersion;
payload["events"][1]["app"]["releaseStage"] = variables.config.releaseStage;
<!--- Add Project API key to payload ---> // Add some metadata to payload
<cfset payload["apiKey"] = variables.config.apiKey /> payload["events"][1]["metaData"] = {};
payload["events"][1]["metaData"]["request"] = {};
payload["events"][1]["metaData"]["request"]["remoteAddr"] = cgi.remote_addr;
payload["events"][1]["metaData"]["request"]["requestMethod"] = cgi.request_method;
payload["events"][1]["metaData"]["request"]["requestUrl"] = cgi.request_url;
<!--- Add notifier info to payload ---> // Send log to Bugsnag
<cfset payload["notifier"] = structNew() /> cfhttp(url="https://notify.bugsnag.com", method="post") {
<cfset payload["notifier"]["name"] = "Taffy" /> cfhttpparam(type="header", name="Content-Type", value="application/json");
<cfset payload["notifier"]["version"] = "1.0" /> cfhttpparam(type="body", value=serializeJSON(payload));
<cfset payload["notifier"]["url"] = "https://github.com/atuttle/Taffy" /> }
}
<!--- Add exception details to payload ---> /**
<cfset payload["events"] = arrayNew(1) /> * Converts the specified exception to be added to the payload
<cfset payload["events"][1] = structNew() /> */
<cfset payload["events"][1]["payloadVersion"] = "2" /> private struct function convertException(required struct exception) output="false" {
<cfset payload["events"][1]["exceptions"] = arrayNew(1) /> var root = "";
<cfset payload["events"][1]["exceptions"][1] = convertException(arguments.exception) /> var exceptionElement = {};
var tagContextElement = "";
<!--- Add application details to payload ---> // Get the root exception
<cfset payload["events"][1]["app"] = structNew() /> if (structKeyExists(exception, "rootCause")) {
<cfset payload["events"][1]["app"]["appVersion"] = variables.config.appVersion /> root = arguments.exception.rootCause;
<cfset payload["events"][1]["app"]["releaseStage"] = variables.config.releaseStage /> } else {
root = arguments.exception;
}
<!--- Add some metadata to payload ---> // Add type and message to payload
<cfset payload["events"][1]["metaData"] = structNew() /> exceptionElement["errorClass"] = root.type;
<cfset payload["events"][1]["metaData"]["request"] = structNew() /> exceptionElement["message"] = root.message;
<cfset payload["events"][1]["metaData"]["request"]["remoteAddr"] = cgi.remote_addr />
<cfset payload["events"][1]["metaData"]["request"]["requestMethod"] = cgi.request_method />
<cfset payload["events"][1]["metaData"]["request"]["requestUrl"] = cgi.request_url />
<!--- Send log to Bugsnag ---> // Build stack trace
<cfhttp url="https://notify.bugsnag.com" method="post"> exceptionElement["stacktrace"] = [];
<cfhttpparam type="header" name="Content-Type" value="application/json" /> for (tagContextElement in root.TagContext) {
<cfhttpparam type="body" value="#serializeJSON(payload)#" /> arrayAppend(exceptionElement["stacktrace"], convertTagContextElement(tagContextElement));
</cfhttp> }
</cffunction>
<!--- return exceptionElement;
Converts the specified exception to be added to the payload }
--->
<cffunction name="convertException" access="private" returntype="struct" output="false">
<cfargument name="exception" type="struct" required="true" />
<!--- Define local variables ---> /**
<cfset var root = "" /> * Converts the specified Tag Context element to be included in the stack trace array
<cfset var exceptionElement = structNew() /> */
<cfset var tagContextElement = "" /> private struct function convertTagContextElement(required struct tagContextElement) output="false" {
var stackTraceElement = {};
<!--- Get the root exception ---> // Add stack trace data
<cfif structKeyExists(exception, "rootCause")> stackTraceElement["file"] = arguments.tagContextElement.template;
<cfset root = arguments.exception.rootCause /> stackTraceElement["lineNumber"] = arguments.tagContextElement.line;
<cfelse> stackTraceElement["columnNumber"] = arguments.tagContextElement.column;
<cfset root = arguments.exception /> stackTraceElement["method"] = "Unknown function";
</cfif>
<!--- Add type and message to payload ---> // Add code details
<cfset exceptionElement["errorClass"] = root.type /> if (structKeyExists(arguments.tagContextElement, "codePrintPlain")) {
<cfset exceptionElement["message"] = root.message /> stackTraceElement["code"] = convertCodePrint(arguments.tagContextElement.codePrintPlain);
}
<!--- Build stack trace ---> return stackTraceElement;
<cfset exceptionElement["stacktrace"] = arrayNew(1) /> }
<cfloop array="#root.TagContext#" index="tagContextElement">
<cfset arrayAppend(exceptionElement["stacktrace"], convertTagContextElement(tagContextElement)) />
</cfloop>
<cfreturn exceptionElement /> /**
</cffunction> * Converts the specified code details to be included to the stack trace element
*/
private struct function convertCodePrint(required string codePrintText) output="false" {
var codeLines = {};
var split = listToArray(arguments.codePrintText, chr(10));
var line = "";
<!--- // Add code lines
Converts the specified Tag Context element to be included in the strack trace array for (line in split) {
----> structInsert(codeLines, listFirst(line, ":"), trim(listRest(line, ":")));
<cffunction name="convertTagContextElement" access="private" returntype="struct" output="false"> }
<cfargument name="tagContextElement" type="struct" required="true" />
<!--- Define local variables ---> return codeLines;
<cfset var stackTraceElement = structNew() /> }
<!--- Add stack trace data ---> }
<cfset stackTraceElement["file"] = arguments.tagContextElement.template />
<cfset stackTraceElement["lineNumber"] = arguments.tagContextElement.line />
<cfset stackTraceElement["columnNumber"] = arguments.tagContextElement.column />
<cfset stackTraceElement["method"] = "Unknown function" />
<!--- Add code details --->
<cfif structKeyExists(arguments.tagContextElement, "codePrintPlain")>
<cfset stackTraceElement["code"] = convertCodePrint(arguments.tagContextElement.codePrintPlain) />
</cfif>
<cfreturn stackTraceElement />
</cffunction>
<!---
Converts the specified code details to be included to the strack trace element
--->
<cffunction name="convertCodePrint" access="private" returntype="struct" output="false">
<cfargument name="codePrintText" type="string" required="true" />
<!--- Define local variables --->
<cfset var codeLines = structNew() />
<cfset var split = listToArray(arguments.codePrintText, chr(10)) />
<cfset var line = "" />
<!--- Add code lines --->
<cfloop array="#split#" index="line">
<cfset structInsert(codeLines, listFirst(line, ":"), trim(listRest(line, ":"))) />
</cfloop>
<cfreturn codeLines />
</cffunction>
</cfcomponent>
+7 -10
View File
@@ -1,13 +1,10 @@
<cfcomponent implements="taffy.bonus.ILogAdapter"> component implements="taffy.bonus.ILogAdapter" {
<cffunction name="init"> public function init(config, tracker) {
<cfargument name="config" /> return this;
<cfargument name="tracker" /> }
<cfreturn this />
</cffunction>
<cffunction name="saveLog"> public function saveLog(exception) {
<cfargument name="exception" /> }
</cffunction>
</cfcomponent> }
+43 -51
View File
@@ -1,61 +1,53 @@
<cfcomponent implements="taffy.bonus.ILogAdapter"> component implements="taffy.bonus.ILogAdapter" {
<cffunction name="init"> public function init(config, tracker="") hint="unused tracker" {
<cfargument name="config" /> variables.config = {};
<cfargument name="tracker" hint="unused" default="" /> structAppend(variables.config, arguments.config, true);
return this;
}
<!--- copy settings into adapter instance data ---> public function saveLog(exception) {
<cfset variables.config = {}> var local = {};
<cfset structAppend( variables.config, arguments.config, true ) />
<cfreturn this /> variables.config = removeEmailPrefix(variables.config);
</cffunction>
<cffunction name="saveLog"> // to conform to the cfmail attribute name and be backward compatible with emailSubj
<cfargument name="exception" /> variables.config.subject = variables.config.subj;
<cfset var local = StructNew() /> local.attributeCollection = variables.config;
<cfset variables.config = removeEmailPrefix(variables.config)> cfmail(attributeCollection=local.attributeCollection) {
if (variables.config.type eq "text") {
writeOutput("Exception Report
<!--- to conform to the cfmail attribute name and be backward compatible with emailSubj ---> Exception Timestamp: #dateformat(now(), 'yyyy-mm-dd')# #timeformat(now(), 'HH:MM:SS tt')#
<cfset variables.config.subject = variables.config.subj>
<cfset local.attributeCollection = variables.config> ");
writeDump(var=arguments.exception, format="text");
if (isDefined('request.debugData')) {
writeDump(var=request.debugData, label="debug data", format="text");
}
} else {
writeOutput("<h2>Exception Report</h2>
<p><strong>Exception Timestamp:</strong> #dateformat(now(), 'yyyy-mm-dd')# #timeformat(now(), 'HH:MM:SS tt')#</p>");
writeDump(arguments.exception);
if (isDefined('request.debugData')) {
writeDump(var=request.debugData, label="debug data");
}
}
}
}
<cfmail attributeCollection="#local.attributeCollection#"> private struct function removeEmailPrefix(required struct configAttributes) output="false" hint="removes all email prefix from the config attributes" {
<cfif variables.config.type eq "text"> var configAttributeName = "";
Exception Report var configAttributeValue = "";
var newConfig = {};
var configAttributeNameWithoutEmailPrefix = "";
for (configAttributeName in arguments.configAttributes) {
configAttributeNameWithoutEmailPrefix = replaceNoCase(configAttributeName, "email", "", "one");
newConfig[configAttributeNameWithoutEmailPrefix] = arguments.configAttributes[configAttributeName];
}
return newConfig;
}
Exception Timestamp: <cfoutput>#dateformat(now(), 'yyyy-mm-dd')# #timeformat(now(), 'HH:MM:SS tt')#</cfoutput> }
<cfdump var="#arguments.exception#" format="text" />
<cfif isDefined('request.debugData')>
<cfdump var="#request.debugData#" label="debug data" format="text"/>
</cfif>
<cfelse>
<h2>Exception Report</h2>
<p><strong>Exception Timestamp:</strong> <cfoutput>#dateformat(now(), 'yyyy-mm-dd')# #timeformat(now(), 'HH:MM:SS tt')#</cfoutput></p>
<cfdump var="#arguments.exception#" />
<cfif isDefined('request.debugData')>
<cfdump var="#request.debugData#" label="debug data" />
</cfif>
</cfif>
</cfmail>
</cffunction>
<cffunction name="removeEmailPrefix" output="false" access="private" returntype="struct" hint="removes all email prefix from the config attributes">
<cfargument name="configAttributes" required="true" type="struct" />
<cfset var configAttributeName="" />
<cfset var configAttributeValue = "" />
<cfset var newConfig = {} />
<cfloop collection="#arguments.configAttributes#" item="configAttributeName">
<cfset configAttributeNameWithoutEmailPrefix = replaceNoCase(configAttributeName, "email", "", "one") />
<cfset newConfig[configAttributeNameWithoutEmailPrefix] = arguments.configAttributes[configAttributeName] />
</cfloop>
<cfreturn newConfig />
</cffunction>
</cfcomponent>
+13 -15
View File
@@ -1,19 +1,17 @@
<cfcomponent implements="taffy.bonus.ILogAdapter"> component implements="taffy.bonus.ILogAdapter" {
<cffunction name="init"> public function init(config, tracker=createObject("component", "Hoth.HothTracker")) {
<cfargument name="config" /> variables.hothtracker = arguments.tracker;
<cfargument name="tracker" default="#createObject("component", "Hoth.HothTracker")#" /> variables.hothtracker.init(
<cfset variables.hothtracker = arguments.tracker />
<cfset variables.hothtracker.init(
createObject("component", arguments.config) createObject("component", arguments.config)
) /> );
<cfreturn this /> return this;
</cffunction> }
<cffunction name="saveLog"> public function saveLog(exception) {
<cfargument name="exception" /> var local = {};
<cfset local.result = variables.HothTracker.track(arguments.exception) /> local.result = variables.HothTracker.track(arguments.exception);
<cfheader name="X-HOTH-LOGGED-EXCEPTION" value="#local.result#" /> cfheader(name="X-HOTH-LOGGED-EXCEPTION", value=local.result);
</cffunction> }
</cfcomponent> }
+14 -20
View File
@@ -1,23 +1,17 @@
<cfcomponent implements="taffy.bonus.ILogAdapter"> component implements="taffy.bonus.ILogAdapter" {
<cffunction name="init"> public function init(config, tracker="") hint="unused tracker" {
<cfargument name="config" /> variables.config = {};
<cfargument name="tracker" hint="unused" default="" /> structAppend(variables.config, arguments.config, true);
return this;
}
<!--- copy settings into adapter instance data ---> public function saveLog(exception) {
<cfset variables.config = {}> var logdump = "";
<cfset structAppend( variables.config, arguments.config, true ) /> savecontent variable="logdump" {
writeDump(var=arguments.exception, format="text");
}
writeLog(file=variables.config.logfile, text=logdump, type="Error");
}
<cfreturn this /> }
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfset var logdump = "">
<!--- build dump --->
<cfsavecontent variable="logdump"><cfdump var="#arguments.exception#" format="text"></cfsavecontent>
<!--- write to log --->
<cflog file="#variables.config.logfile#" text="#logdump#" type="Error">
</cffunction>
</cfcomponent>
+14 -17
View File
@@ -1,20 +1,17 @@
<cfcomponent implements="taffy.bonus.ILogAdapter"> component implements="taffy.bonus.ILogAdapter" {
<cffunction name="init"> public function init(config, tracker="") hint="unused tracker" {
<cfargument name="config" /> return this;
<cfargument name="tracker" hint="unused" default="" /> }
<cfreturn this />
</cffunction>
<cffunction name="saveLog"> public function saveLog(exception) {
<cfargument name="exception" /> cfcontent(type="text/html");
<cfcontent type="text/html" /> cfheader(statuscode="500", statustext="Unhandled API Error");
<cfheader statuscode="500" statustext="Unhandled API Error" /> writeDump(arguments);
<cfdump var="#arguments#" /> if (isDefined('request.debugData')) {
<cfif isDefined('request.debugData')> writeDump(var=request.debugData, label="debug data");
<cfdump var="#request.debugData#" label="debug data" /> }
</cfif> abort;
<cfabort /> }
</cffunction>
</cfcomponent> }
+80 -62
View File
@@ -1,64 +1,82 @@
{ {
"name":"Taffy", "name": "Taffy",
"shortDescription":"REST Web Service framework for ColdFusion and Lucee", "shortDescription": "REST Web Service framework for ColdFusion and Lucee",
"slug":"taffy", "slug": "taffy",
"author":"Adam Tuttle", "author": "Adam Tuttle",
"version":"3.3.0", "version": "4.0.0",
"homepage":"https://taffy.io/", "homepage": "https://taffy.io/",
"documentation":"https://docs.taffy.io/", "documentation": "https://docs.taffy.io/",
"type":"mvc", "type": "mvc",
"repository":{ "repository": {
"type":"git", "type": "git",
"url":"https://github.com/atuttle/Taffy.git" "url": "https://github.com/atuttle/Taffy.git"
}, },
"bugs":"https://github.com/atuttle/Taffy/issues", "bugs": "https://github.com/atuttle/Taffy/issues",
"license":[ "license": [
{ {
"type":"MIT", "type": "MIT",
"url":"https://raw.githubusercontent.com/atuttle/Taffy/main/LICENSE.TXT" "url": "https://raw.githubusercontent.com/atuttle/Taffy/main/LICENSE.TXT"
} }
], ],
"devDependencies":{ "devDependencies": {
"testbox":"^4.2.1+400", "testbox": "^6.5.0",
"commandbox-cfconfig":"^1.0.2", "commandbox-cfconfig": "^1.0.2",
"di1":"git+https://github.com/framework-one/di1.git", "di1": "git+https://github.com/framework-one/di1.git",
"hoth":"git+https://github.com/aarongreenlee/Hoth.git", "hoth": "git+https://github.com/aarongreenlee/Hoth.git",
"BugLogHQ":"git+https://github.com/oarevalo/BugLogHQ.git" "BugLogHQ": "git+https://github.com/oarevalo/BugLogHQ.git"
}, },
"installPaths":{ "installPaths": {
"testbox":"tests/testbox/", "testbox": "tests/testbox/",
"di1":"tests/di1/", "di1": "tests/di1/",
"Hoth":"tests/Hoth/", "Hoth": "tests/Hoth/",
"BugLogHQ":"tests/BugLogHQ/" "BugLogHQ": "tests/BugLogHQ/"
}, },
"engines":[ "engines": [
{ {
"type":"lucee", "type": "lucee",
"version":">=4.5.x" "version": ">=5.x.x"
}, },
{ {
"type":"adobe", "type": "adobe",
"version":">=8.0.0" "version": ">=2016"
} }
], ],
"ignore":[ "ignore": [
"examples/", "examples/",
"lib/", "lib/",
"snippets/", "snippets/",
"tests/", "tests/",
".cfconfig.json", ".cfconfig.json",
".gitignore", ".gitignore",
".travis.yml", ".travis.yml",
"CONTRIBUTING.md", "CONTRIBUTING.md",
"build.xml", "build.xml",
"server.json" "server.json"
], ],
"testbox": { "testbox": {
"runner": [ "runner": [
{ {
"default": "http://localhost/runner.cfm" "default": "http://localhost:8080/tests/runner.cfm"
} }
], ],
"directory": "tests.specs" "directory": "tests/specs",
} "bundles": "",
"labels": "",
"excludes": "",
"verbose": true
},
"scripts": {
"start:all": "server start serverConfigFile=server-lucee5.json && server start serverConfigFile=server-lucee6.json && server start serverConfigFile=server-lucee7.json",
"start:lucee5": "server start serverConfigFile=server-lucee5.json",
"start:lucee6": "server start serverConfigFile=server-lucee6.json",
"start:lucee7": "server start serverConfigFile=server-lucee7.json",
"stop:lucee5": "server stop name=taffy-lucee5",
"stop:lucee6": "server stop name=taffy-lucee6",
"stop:lucee7": "server stop name=taffy-lucee7",
"stop:all": "server stop --all",
"test:all": "testbox run runner=http://localhost:8085/tests/runner.cfm && testbox run runner=http://localhost:8086/tests/runner.cfm && testbox run runner=http://localhost:8087/tests/runner.cfm",
"test:lucee5": "testbox run runner=http://localhost:8085/tests/runner.cfm",
"test:lucee6": "testbox run runner=http://localhost:8086/tests/runner.cfm",
"test:lucee7": "testbox run runner=http://localhost:8087/tests/runner.cfm"
}
} }
-109
View File
@@ -1,109 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
This is an ANT script that runs the MXUnit Integration tests for Taffy
Step 1 in continuous integration!
It assumes that you're running taffy from your web root. Never tried
from an external path + mapping.
-->
<project name="Taffy Integration Tests" basedir="." default="runtests">
<!-- define non-standard tasks -->
<taskdef resource="org/eclipse/jgit/ant/ant-tasks.properties">
<classpath>
<pathelement location="lib/org.eclipse.jgit.ant-1.0.99.0.6-UNOFFICIAL-ROBERTO-RELEASE.jar"/>
<pathelement location="lib/org.eclipse.jgit-2.0.0.201206130900-r.jar"/>
<pathelement location="lib/jsch-0.1.48.jar"/>
</classpath>
</taskdef>
<target name="init">
<!-- //////// DIRECTORY AND CFC PATH SETUP (used in all targets) -->
<!-- what's the directory name of your application? this value will be used throughout this build file;
if you don't want that, just replace the references to ${application.name} with your desired values -->
<property name="application.name" value="taffy" />
<!-- what's the name of the directory where your tests live? Note: this is just the name
of the directory, not the full path-->
<property name="test.dir.name" value="tests/tests" />
<!-- where do your tests live, relative to this build file? test.dir.location will be a
full path to a directory -->
<property name="test.dir.location" location="tests/tests" />
<!-- what is the cfc dot-notation path to that directory, as ColdFusion sees it? -->
<property name="test.cfcpath" value="taffy.tests.tests" />
<!-- //////// MXUNIT ANT TASK SETUP (used in runtests and junitreport targets) -->
<!-- what server and port should your tests run against? -->
<property name="test.server" value="jenkins.local" />
<property name="test.serverport" value="80" />
<!-- what "runner" URL should the tests hit. In this example, you'd be hitting
http://localhost:80/DirectoryNameOfYourProject/test/HttpAntRunner.cfc
Simply copy mxunit/samples/HttpAntRunner.cfc into your test directory! -->
<property name="test.runner" value="/${application.name}/${test.dir.name}/HttpAntRunner.cfc" />
<!-- this is where the xml and html will live for the report generator -->
<property name="test.output" location="${test.dir.name}/output" />
<property name="test.output.xml" location="${test.output}/xml" />
<property name="test.junitoutput" location="${test.output}/html" />
<!-- //////// JAR FILES WE NEED FOR EXTERNAL TASKS -->
<!-- where does the mxunit ant jar file live? it's easiest to copy it out of the mxunit install and put it into your app
You can also put any other ant-relatd jars in this directory; for example, if you want to use svnant, you'll need to put those jars here
-->
<path id="project.classpath">
<fileset dir="lib">
<include name="**/*.jar" />
</fileset>
</path>
<!-- dump the properties -->
<echoproperties prefix="test" />
</target>
<target name="clean" depends="init">
<mkdir dir="${test.output}" />
<mkdir dir="${test.output.xml}" />
<mkdir dir="${test.junitoutput}" />
</target>
<target name="runtests" description="Make output directories and run the MXUnit task" depends="init,clean">
<!-- first, make sure Taffy is initialized -->
<get src="http://jenkins.local/taffy/tests/?dashboard&amp;reload=true" dest="init.result" />
<delete file="init.result" />
<!-- <delete dir="${test.output}" failonerror="false" /> -->
<taskdef name="mxunittask" classname="org.mxunit.ant.MXUnitAntTask" classpathref="project.classpath" />
<mxunittask
server="${test.server}"
port="${test.serverport}"
defaultrunner="${test.runner}"
outputdir="${test.output.xml}"
verbose="true"
failureproperty="testsfailed">
<directory path="${test.dir.location}" recurse="true" packageName="${test.cfcpath}" componentPath="${test.cfcpath}" />
</mxunittask>
<fail if="testsfailed" message="Build fails because tests failed" />
<!-- generate pretty reports -->
<antcall target="junitreport" />
</target>
<target name="junitreport" depends="init" description="Runs the report without running the tests">
<junitreport todir="${test.junitoutput}">
<fileset dir="${test.output.xml}">
<include name="*.xml" />
</fileset>
<report format="frames" todir="${test.junitoutput}" />
</junitreport>
</target>
</project>
+1390 -1360
View File
File diff suppressed because it is too large Load Diff
+40 -50
View File
@@ -1,57 +1,47 @@
<cfcomponent output="false" hint="a helper class to decode input data"> component output="false" hint="a helper class to decode input data" {
<!--- the most basic of input handlers, available in all APIs ---> public function getFromForm(body) output="false" taffy_mime="application/x-www-form-urlencoded" hint="get data from form post" {
<cffunction name="getFromForm" output="false" taffy:mime="application/x-www-form-urlencoded" hint="get data from form post"> var response = {};
<cfargument name="body" hint="the textual request body" /> var pairs = listToArray(arguments.body, "&");
<cfset var response = {} /> var pair = "";
<cfset var pairs = listToArray(arguments.body, "&") /> var kv = [];
<cfset var pair = "" /> var ix = 0;
<cfset var kv = [] /> var k = "";
<cfset var ix = 0 /> var v = "";
<cfset var k = "" />
<cfset var v = "" />
<cfif not find('=', arguments.body)> if (!find('=', arguments.body)) {
<cfset throwError(400, "You've indicated that you're sending form-encoded data but it doesn't appear to be valid. Aborting request.") /> throwError(400, "You've indicated that you're sending form-encoded data but it doesn't appear to be valid. Aborting request.");
</cfif> }
<cfloop from="1" to="#arrayLen(pairs)#" index="ix"> for (ix = 1; ix <= arrayLen(pairs); ix++) {
<cfset pair = pairs[ix] /> pair = pairs[ix];
<cfset kv = listToArray(pair, "=", true) /> kv = listToArray(pair, "=", true);
<cfset k = kv[1] /> k = kv[1];
<cfset v = urlDecode( kv[2] ) /> v = urlDecode(kv[2]);
<cfif structKeyExists( response, k )> if (structKeyExists(response, k)) {
<cfset response[k] = listAppend(response[k], v)> response[k] = listAppend(response[k], v);
<cfelse> } else {
<cfset response[k] = v> response[k] = v;
</cfif> }
</cfloop> }
<cfreturn response /> return response;
</cffunction> }
<!--- ============================ ---> private void function throwError(numeric statusCode=500, required string msg, struct headers={}) output="false" {
<!--- Helpers ---> cfcontent(reset="true");
<!--- ============================ ---> addHeaders(arguments.headers);
cfheader(statuscode=arguments.statusCode, statustext=arguments.msg);
abort;
}
<cffunction name="throwError" access="private" output="false" returntype="void"> private void function addHeaders(required struct headers) output="false" {
<cfargument name="statusCode" type="numeric" default="500" /> var h = '';
<cfargument name="msg" type="string" required="true" hint="message to return to api consumer" /> if (!structIsEmpty(arguments.headers)) {
<cfargument name="headers" type="struct" required="false" default="#structNew()#" /> for (h in arguments.headers) {
<cfcontent reset="true" /> cfheader(name=h, value=arguments.headers[h]);
<cfset addHeaders(arguments.headers) /> }
<cfheader statuscode="#arguments.statusCode#" statustext="#arguments.msg#" /> }
<cfabort /> }
</cffunction>
<cffunction name="addHeaders" access="private" output="false" returntype="void"> }
<cfargument name="headers" type="struct" required="true" />
<cfset var h = '' />
<cfif !structIsEmpty(arguments.headers)>
<cfloop list="#structKeyList(arguments.headers)#" index="h">
<cfheader name="#h#" value="#arguments.headers[h]#" />
</cfloop>
</cfif>
</cffunction>
</cfcomponent>
+173 -177
View File
@@ -1,199 +1,195 @@
<cfcomponent output="false" hint="a helper class to represent easily serializable data"> component output="false" hint="a helper class to represent easily serializable data" {
<cfset variables.data = "" /> variables.data = "";
<cfset variables.fileName = "" /> variables.fileName = "";
<cfset variables.fileMime = "" /> variables.fileMime = "";
<cfset variables.statusCode = 200 /> variables.statusCode = 200;
<cfset variables.statusText = "OK" /> variables.statusText = "OK";
<cfset variables.miscHeaders = StructNew() /> variables.miscHeaders = {};
<cfset variables.deleteFile = false /> variables.deleteFile = false;
<!--- 1= textual, 2= filename, 3= file data ---> variables.noDataSends204NoContent = false;
<cfset variables.type = 1 /> // 1= textual, 2= filename, 3= file data
<cfset variables.types = StructNew() /> variables.type = 1;
<cfset variables.types[1] = "textual" /> variables.types = {};
<cfset variables.types[2] = "filename" /> variables.types[1] = "textual";
<cfset variables.types[3] = "filedata" /> variables.types[2] = "filename";
<cfset variables.types[4] = "imagedata" /> variables.types[3] = "filedata";
<cfset variables.statusTexts = StructNew() /> variables.types[4] = "imagedata";
<cfset variables.statusTexts[100] = "Continue" /> variables.statusTexts = {};
<cfset variables.statusTexts[101] = "Switching Protocols" /> variables.statusTexts[100] = "Continue";
<cfset variables.statusTexts[102] = "Processing" /> variables.statusTexts[101] = "Switching Protocols";
<cfset variables.statusTexts[200] = "OK" /> variables.statusTexts[102] = "Processing";
<cfset variables.statusTexts[201] = "Created" /> variables.statusTexts[200] = "OK";
<cfset variables.statusTexts[202] = "Accepted" /> variables.statusTexts[201] = "Created";
<cfset variables.statusTexts[203] = "Non-authoritative Information" /> variables.statusTexts[202] = "Accepted";
<cfset variables.statusTexts[204] = "No Content" /> variables.statusTexts[203] = "Non-authoritative Information";
<cfset variables.statusTexts[205] = "Reset Content" /> variables.statusTexts[204] = "No Content";
<cfset variables.statusTexts[206] = "Partial Content" /> variables.statusTexts[205] = "Reset Content";
<cfset variables.statusTexts[207] = "Multi-Status" /> variables.statusTexts[206] = "Partial Content";
<cfset variables.statusTexts[208] = "Already Reported" /> variables.statusTexts[207] = "Multi-Status";
<cfset variables.statusTexts[226] = "IM Used" /> variables.statusTexts[208] = "Already Reported";
<cfset variables.statusTexts[300] = "Multiple Choices" /> variables.statusTexts[226] = "IM Used";
<cfset variables.statusTexts[301] = "Moved Permanently" /> variables.statusTexts[300] = "Multiple Choices";
<cfset variables.statusTexts[302] = "Found" /> variables.statusTexts[301] = "Moved Permanently";
<cfset variables.statusTexts[303] = "See Other" /> variables.statusTexts[302] = "Found";
<cfset variables.statusTexts[304] = "Not Modified" /> variables.statusTexts[303] = "See Other";
<cfset variables.statusTexts[305] = "Use Proxy" /> variables.statusTexts[304] = "Not Modified";
<cfset variables.statusTexts[307] = "Temporary Redirect" /> variables.statusTexts[305] = "Use Proxy";
<cfset variables.statusTexts[308] = "Permanent Redirect" /> variables.statusTexts[307] = "Temporary Redirect";
<cfset variables.statusTexts[400] = "Bad Request" /> variables.statusTexts[308] = "Permanent Redirect";
<cfset variables.statusTexts[401] = "Unauthorized" /> variables.statusTexts[400] = "Bad Request";
<cfset variables.statusTexts[402] = "Payment Required" /> variables.statusTexts[401] = "Unauthorized";
<cfset variables.statusTexts[403] = "Forbidden" /> variables.statusTexts[402] = "Payment Required";
<cfset variables.statusTexts[404] = "Not Found" /> variables.statusTexts[403] = "Forbidden";
<cfset variables.statusTexts[405] = "Method Not Allowed" /> variables.statusTexts[404] = "Not Found";
<cfset variables.statusTexts[406] = "Not Acceptable" /> variables.statusTexts[405] = "Method Not Allowed";
<cfset variables.statusTexts[407] = "Proxy Authentication Required" /> variables.statusTexts[406] = "Not Acceptable";
<cfset variables.statusTexts[408] = "Request Timeout" /> variables.statusTexts[407] = "Proxy Authentication Required";
<cfset variables.statusTexts[409] = "Conflict" /> variables.statusTexts[408] = "Request Timeout";
<cfset variables.statusTexts[410] = "Gone" /> variables.statusTexts[409] = "Conflict";
<cfset variables.statusTexts[411] = "Length Required" /> variables.statusTexts[410] = "Gone";
<cfset variables.statusTexts[412] = "Precondition Failed" /> variables.statusTexts[411] = "Length Required";
<cfset variables.statusTexts[413] = "Payload Too Large" /> variables.statusTexts[412] = "Precondition Failed";
<cfset variables.statusTexts[414] = "Request-URI Too Long" /> variables.statusTexts[413] = "Payload Too Large";
<cfset variables.statusTexts[415] = "Unsupported Media Type" /> variables.statusTexts[414] = "Request-URI Too Long";
<cfset variables.statusTexts[416] = "Requested Range Not Satisfiable" /> variables.statusTexts[415] = "Unsupported Media Type";
<cfset variables.statusTexts[417] = "Expectation Failed" /> variables.statusTexts[416] = "Requested Range Not Satisfiable";
<cfset variables.statusTexts[418] = "I'm a teapot" /> variables.statusTexts[417] = "Expectation Failed";
<cfset variables.statusTexts[421] = "Misdirected Request" /> variables.statusTexts[418] = "I'm a teapot";
<cfset variables.statusTexts[422] = "Unprocessable Entity" /> variables.statusTexts[421] = "Misdirected Request";
<cfset variables.statusTexts[423] = "Locked" /> variables.statusTexts[422] = "Unprocessable Entity";
<cfset variables.statusTexts[424] = "Failed Dependency" /> variables.statusTexts[423] = "Locked";
<cfset variables.statusTexts[426] = "Upgrade Required" /> variables.statusTexts[424] = "Failed Dependency";
<cfset variables.statusTexts[428] = "Precondition Required" /> variables.statusTexts[426] = "Upgrade Required";
<cfset variables.statusTexts[429] = "Too Many Requests" /> variables.statusTexts[428] = "Precondition Required";
<cfset variables.statusTexts[431] = "Request Header Fields Too Large" /> variables.statusTexts[429] = "Too Many Requests";
<cfset variables.statusTexts[444] = "Connection Closed Without Response" /> variables.statusTexts[431] = "Request Header Fields Too Large";
<cfset variables.statusTexts[451] = "Unavailable For Legal Reasons" /> variables.statusTexts[444] = "Connection Closed Without Response";
<cfset variables.statusTexts[499] = "Client Closed Request" /> variables.statusTexts[451] = "Unavailable For Legal Reasons";
<cfset variables.statusTexts[500] = "Internal Server Error" /> variables.statusTexts[499] = "Client Closed Request";
<cfset variables.statusTexts[501] = "Not Implemented" /> variables.statusTexts[500] = "Internal Server Error";
<cfset variables.statusTexts[502] = "Bad Gateway" /> variables.statusTexts[501] = "Not Implemented";
<cfset variables.statusTexts[503] = "Service Unavailable" /> variables.statusTexts[502] = "Bad Gateway";
<cfset variables.statusTexts[504] = "Gateway Timeout" /> variables.statusTexts[503] = "Service Unavailable";
<cfset variables.statusTexts[505] = "HTTP Version Not Supported" /> variables.statusTexts[504] = "Gateway Timeout";
<cfset variables.statusTexts[506] = "Variant Also Negotiates" /> variables.statusTexts[505] = "HTTP Version Not Supported";
<cfset variables.statusTexts[507] = "Insufficient Storage" /> variables.statusTexts[506] = "Variant Also Negotiates";
<cfset variables.statusTexts[508] = "Loop Detected" /> variables.statusTexts[507] = "Insufficient Storage";
<cfset variables.statusTexts[510] = "Not Extended" /> variables.statusTexts[508] = "Loop Detected";
<cfset variables.statusTexts[511] = "Network Authentication Required" /> variables.statusTexts[510] = "Not Extended";
<cfset variables.statusTexts[599] = "Network Connect Timeout Error" /> variables.statusTexts[511] = "Network Authentication Required";
variables.statusTexts[599] = "Network Connect Timeout Error";
<cffunction name="getType" acces="public" output="false"> public function getType() output="false" {
<cfreturn variables.types[variables.type] /> return variables.types[variables.type];
</cffunction> }
<cffunction name="setData" access="public" output="false" hint="setter for the data to be returned"> public function setData(required data) output="false" hint="setter for the data to be returned" {
<cfargument name="data" required="true" hint="the simple or complex data that you want to return to the api consumer" /> variables.type = 1;
<cfset variables.type = 1 /> variables.data = arguments.data;
<cfset variables.data = arguments.data /> return this;
<cfreturn this /> }
</cffunction>
<cffunction name="getData" access="public" output="false" hint="mostly for testability, returns the native data embedded in the representation instance"> public function getData() output="false" hint="mostly for testability, returns the native data embedded in the representation instance" {
<cfreturn variables.data /> return variables.data;
</cffunction> }
<cffunction name="noData" access="public" output="false" hint="returns empty representation instance"> public function setNoDataSends204NoContent(required boolean value) output="false" {
<cfif application._taffy.settings.noDataSends204NoContent> variables.noDataSends204NoContent = arguments.value;
<cfreturn this.noContent() /> return this;
<cfelse> }
<cfreturn this />
</cfif>
</cffunction>
<cffunction name="noContent" access="public" output="false" hint="returns empty representation instance"> public function noData() output="false" hint="returns empty representation instance" {
<!--- According to issue #365 https://github.com/atuttle/Taffy/issues/365 if (variables.noDataSends204NoContent) {
noContent() returns with HTTP status code 204 and Content-Type as text/plain (omitting this header is difficult and maybe not recommanded) return this.noContent();
noData() is kept 'as is' for backward compatibility with existing implementations } else {
---> return this;
<cfreturn this.withStatus(204).withHeaders({"Content-Type"="text/plain"}) /> }
</cffunction> }
<cffunction name="setFileName" access="public" output="false" hint="Pass in a file-name (fully qualified, e.g. c:\temp\img.jpg) to have Taffy stream this file back to the client"> public function noContent() output="false" hint="returns empty representation instance" {
<cfargument name="file" type="string" required="true" /> // According to issue #365 https://github.com/atuttle/Taffy/issues/365
<cfset variables.type = 2 /> // noContent() returns with HTTP status code 204 and Content-Type as text/plain (omitting this header is difficult and maybe not recommanded)
<cfset variables.fileName = arguments.file /> // noData() is kept 'as is' for backward compatibility with existing implementations
<cfreturn this /> return this.withStatus(204).withHeaders({"Content-Type"="text/plain"});
</cffunction> }
<cffunction name="getFileName" access="public" output="false"> public function setFileName(required string file) output="false" hint="Pass in a file-name (fully qualified, e.g. c:\temp\img.jpg) to have Taffy stream this file back to the client" {
<cfreturn variables.fileName /> variables.type = 2;
</cffunction> variables.fileName = arguments.file;
return this;
}
<cffunction name="setFileData" access="public" output="false" hint="Pass in file data (eg a generated PDF object) - NOT a Filename! - to have Taffy stream the content back to the client"> public function getFileName() output="false" {
<cfargument name="data" required="true" /> return variables.fileName;
<cfset variables.type = 3 /> }
<cfset variables.fileData = arguments.data />
<cfreturn this />
</cffunction>
<cffunction name="getFileData" access="public" output="false"> public function setFileData(required data) output="false" hint="Pass in file data (eg a generated PDF object) - NOT a Filename! - to have Taffy stream the content back to the client" {
<cfreturn variables.fileData /> variables.type = 3;
</cffunction> variables.fileData = arguments.data;
return this;
}
<cffunction name="setImageData" access="public" output="false" hint="Pass in image data (eg a generated image object) - NOT a Filename! - to have Taffy stream the content back to the client"> public function getFileData() output="false" {
<cfargument name="data" required="true" /> return variables.fileData;
<cfset variables.type = 4 /> }
<cfif not isBinary(arguments.data)>
<cfset arguments.data = toBinary(toBase64(arguments.data)) />
</cfif>
<cfset variables.fileData = arguments.data />
<cfreturn this />
</cffunction>
<cffunction name="getImageData" access="public" output="false"> public function setImageData(required data) output="false" hint="Pass in image data (eg a generated image object) - NOT a Filename! - to have Taffy stream the content back to the client" {
<cfreturn getFileData() /> variables.type = 4;
</cffunction> if (!isBinary(arguments.data)) {
arguments.data = toBinary(toBase64(arguments.data));
}
variables.fileData = arguments.data;
return this;
}
<cffunction name="withMime" access="public" output="false" hint="Use this method in conjunction with streamFile and streamBinary in your resources to set the mime type of the file being returned. Ex: return streamFile('kittens/cuteness.jpg').withMime('image/jpeg');"> public function getImageData() output="false" {
<cfargument name="mime" type="string" required="true" /> return getFileData();
<cfset variables.fileMime = arguments.mime /> }
<cfreturn this />
</cffunction>
<cffunction name="getFileMime" access="public" output="false"> public function withMime(required string mime) output="false" hint="Use this method in conjunction with streamFile and streamBinary in your resources to set the mime type of the file being returned. Ex: return streamFile('kittens/cuteness.jpg').withMime('image/jpeg');" {
<cfreturn variables.fileMime /> variables.fileMime = arguments.mime;
</cffunction> return this;
}
<cffunction name="withStatus" access="public" output="false" hint="used to set the http response code for the response"> public function getFileMime() output="false" {
<cfargument name="statusCode" type="numeric" required="true" hint="eg 200" /> return variables.fileMime;
<cfargument name="statusText" type="string" required="false" default="" /> }
<cfset variables.statusCode = arguments.statusCode />
<cfif len(arguments.statusText)>
<cfset variables.statusText = arguments.statusText />
<cfelseif StructKeyExists(variables.statusTexts, arguments.statusCode)>
<cfset variables.statusText = variables.statusTexts[arguments.statusCode] />
</cfif>
<cfreturn this />
</cffunction>
<cffunction name="getStatus" access="public" output="false" returnType="numeric"> public function withStatus(required numeric statusCode, string statusText="") output="false" hint="used to set the http response code for the response" {
<cfreturn variables.statusCode /> variables.statusCode = arguments.statusCode;
</cffunction> if (len(arguments.statusText)) {
variables.statusText = arguments.statusText;
} else if (structKeyExists(variables.statusTexts, arguments.statusCode)) {
variables.statusText = variables.statusTexts[arguments.statusCode];
}
return this;
}
<cffunction name="getStatusText" access="public" output="false" returnType="string"> public numeric function getStatus() output="false" {
<cfreturn variables.statusText /> return variables.statusCode;
</cffunction> }
<cffunction name="withHeaders" access="public" output="false" hint="used to set custom headers for the response"> public string function getStatusText() output="false" {
<cfargument name="headerStruct" type="struct" required="true" /> return variables.statusText;
<cfset variables.miscHeaders = arguments.headerStruct /> }
<cfreturn this />
</cffunction>
<cffunction name="getHeaders" access="public" output="false" returntype="Struct"> public function withHeaders(required struct headerStruct) output="false" hint="used to set custom headers for the response" {
<cfreturn variables.miscHeaders /> variables.miscHeaders = arguments.headerStruct;
</cffunction> return this;
}
<cffunction name="andDelete" access="public" output="false" hint="used to delete the streamed file"> public struct function getHeaders() output="false" {
<cfargument name="doDeleteFile" type="boolean" required="true" /> return variables.miscHeaders;
<cfset variables.deleteFile = arguments.doDeleteFile /> }
<cfreturn this />
</cffunction>
<cffunction name="getDeleteFile" access="public" output="false" returntype="boolean"> public function andDelete(required boolean doDeleteFile) output="false" hint="used to delete the streamed file" {
<cfreturn variables.deleteFile /> variables.deleteFile = arguments.doDeleteFile;
</cffunction> return this;
}
</cfcomponent> public boolean function getDeleteFile() output="false" {
return variables.deleteFile;
}
}
+166 -174
View File
@@ -1,159 +1,138 @@
<cfcomponent output="false"> component output="false" {
<cfscript>
//bean cache
this.beans = structNew();
this.transients = structNew();
//functionality
</cfscript>
<cffunction name="init" output="false"> // bean cache
<cfargument name="externalBeanFactory"> this.beans = {};
<cfscript> this.transients = {};
if (structKeyExists(arguments, "externalBeanFactory")) {
this.externalBeanFactory = arguments.externalBeanFactory;
}
return this;
</cfscript>
</cffunction>
<cfscript> public function init(externalBeanFactory) {
// Proxy to beanExists to provide similar interface to ColdSpring if (structKeyExists(arguments, "externalBeanFactory")) {
function containsBean(beanName){ this.externalBeanFactory = arguments.externalBeanFactory;
return beanExists(arguments.beanName);
} }
function transientExists(beanName){ return this;
return structKeyExists(this.transients, arguments.beanName); }
// Proxy to beanExists to provide similar interface to ColdSpring
public function containsBean(beanName) {
return beanExists(arguments.beanName);
}
public function transientExists(beanName) {
return structKeyExists(this.transients, arguments.beanName);
}
public function getBean(beanName) {
var b = 0;
var meta = 0;
if (beanExists(arguments.beanName, false, false)) {
return this.beans[arguments.beanName];
} else if (transientExists(arguments.beanName)) {
b = createObject('component', this.transients[arguments.beanName]);
meta = getMetadata(b);
_recurse_ResolveDependencies(b, meta);
return b;
} else if (externalBeanExists(arguments.beanName)) {
return this.externalBeanFactory.getBean(arguments.beanName);
} else {
throwError(message="Bean name '#arguments.beanName#' not found.", type="Taffy.Factory.BeanNotFound");
} }
function getBean(beanName){ }
var b = 0;
var meta = 0; public function getBeanList() {
if (beanExists(arguments.beanName, false, false)){ var combined = structKeyList(this.beans);
return this.beans[arguments.beanName]; var trans = structKeyList(this.transients);
}else if (transientExists(arguments.beanName)){ if (len(combined) and len(trans)) {
b = createObject('component', this.transients[arguments.beanName]); combined = combined & ",";
meta = getMetadata(b); }
_recurse_ResolveDependencies(b, meta); combined = combined & trans;
return b; return combined;
}else if (externalBeanExists(arguments.beanName)){ }
return this.externalBeanFactory.getBean(arguments.beanName);
}else{ public function beanExists(required beanName, includeTransients=true, includeExternal=false) output="false" {
throwError(message="Bean name '#arguments.beanName#' not found.", type="Taffy.Factory.BeanNotFound"); return structKeyExists(this.beans, arguments.beanName) or (arguments.includeTransients and transientExists(arguments.beanName)) or
(arguments.includeExternal and externalBeanExists(arguments.beanName));
}
private boolean function externalBeanExists(required beanName) output="false" {
return structKeyExists(this, "externalBeanFactory") and this.externalBeanFactory.containsBean(arguments.beanName);
}
public void function loadBeansFromPath(
required string beanPath hint="Absolute path to folder containing beans",
string resourcesPath="resources",
string resourcesBasePath="",
boolean isFullReload=false,
taffyRef={}
) output="false" {
var local = {};
// cache all of the beans
if (isFullReload) {
this.beans = {};
arguments.taffyRef.status.skippedResources = []; // empty out the array on factory reloads
arguments.taffyRef.beanList = "";
}
// if the folder doesn't exist, do nothing
if (!directoryExists(arguments.beanPath)) {
return;
}
// get list of beans to load
local.beanQuery = directoryList(arguments.beanPath, true, "query", "*.cfc");
for (var row in local.beanQuery) {
local.beanName = filePathToBeanName(row.directory, row.name, arguments.resourcesBasePath);
local.beanPath = filePathToBeanPath(row.directory, row.name, arguments.resourcesPath, arguments.resourcesBasePath);
try {
local.objBean = createObject("component", local.beanPath);
if (isSerializerInstance(local.objBean)) {
this.transients[local.beanName] = local.beanPath;
} else {
this.beans[local.beanName] = local.objBean;
}
} catch (any e) {
// skip cfc's with errors, but save info about them for display in the dashboard
local.err = {};
local.err.resource = local.beanName;
local.err.exception = e;
arrayAppend(arguments.taffyRef.status.skippedResources, local.err);
} }
} }
function getBeanList(){ // resolve dependencies
var combined = structKeyList(this.beans); for (local.b in this.beans) {
var trans = structKeyList(this.transients); local.bean = this.beans[local.b];
if (len(combined) and len(trans)){ local.beanMeta = getMetadata(local.bean);
combined = combined & ","; _recurse_ResolveDependencies(local.bean, local.beanMeta);
}
combined = combined & trans;
return combined;
} }
</cfscript> }
<cffunction name="beanExists" output="false">
<cfargument required="true" name="beanName">
<cfargument name="includeTransients" default="true">
<cfargument name="includeExternal" default="false">
<cfscript>
return structKeyExists(this.beans, arguments.beanName) or (arguments.includeTransients and transientExists(arguments.beanName)) or
(arguments.includeExternal and externalBeanExists(arguments.beanName));
</cfscript>
</cffunction>
<cffunction name="externalBeanExists" access="private" output="false" returnType="boolean">
<cfargument required="true" name="beanName">
<cfscript>
return structKeyExists(this, "externalBeanFactory") and this.externalBeanFactory.containsBean(arguments.beanName);
</cfscript>
</cffunction>
<cffunction name="loadBeansFromPath" access="public" output="false" returnType="void">
<cfargument name="beanPath" type="string" required="true" hint="Absolute path to folder containing beans" />
<cfargument name="resourcesPath" type="string" default="resources" />
<cfargument name="resourcesBasePath" type="string" default="" />
<cfargument name="isFullReload" type="boolean" default="false" />
<cfargument name="taffyRef" type="any" required="false" default="#structNew()#" />
<cfset var local = StructNew() />
<!--- cache all of the beans --->
<cfif isFullReload>
<cfset this.beans = structNew() />
<cfset arguments.taffyRef.status.skippedResources = arrayNew(1) /> <!--- empty out the array on factory reloads --->
<cfset arguments.taffyRef.beanList = "" />
</cfif>
<!--- if the folder doesn't exist, do nothing --->
<cfif not directoryExists(arguments.beanPath)>
<cfreturn />
</cfif>
<!--- get list of beans to load --->
<cfdirectory action="list" directory="#arguments.beanPath#" filter="*.cfc" name="local.beanQuery" recurse="true" />
<cfloop query="local.beanQuery">
<cfset local.beanName = filePathToBeanName(local.beanQuery.directory, local.beanquery.name, arguments.resourcesBasePath) />
<cfset local.beanPath = filePathToBeanPath(local.beanQuery.directory, local.beanquery.name, arguments.resourcesPath, arguments.resourcesBasePath) />
<cftry>
<cfset local.objBean = createObject("component", local.beanPath) />
<cfif isInstanceOf(local.objBean, "taffy.core.baseSerializer")>
<cfset this.transients[local.beanName] = local.beanPath />
<cfelse>
<cfset this.beans[local.beanName] = local.objBean />
</cfif>
<cfcatch>
<!--- skip cfc's with errors, but save info about them for display in the dashboard --->
<cfset local.err = structNew() />
<cfset local.err.resource = local.beanName />
<cfset local.err.exception = cfcatch />
<cfset arrayAppend(arguments.taffyRef.status.skippedResources, local.err) />
</cfcatch>
</cftry>
</cfloop>
<!--- resolve dependencies --->
<cfloop list="#structKeyList(this.beans)#" index="local.b">
<cfset local.bean = this.beans[local.b] />
<cfset local.beanMeta = getMetadata(local.bean) />
<cfset _recurse_ResolveDependencies(local.bean, local.beanMeta) />
</cfloop>
</cffunction>
<cffunction name="filePathToBeanPath" access="private"> private function filePathToBeanPath(path, filename, resourcesPath, resourcesBasePath) {
<cfargument name="path" /> var beanPath = "";
<cfargument name="filename" /> if (len(resourcesBasePath) eq 0) {
<cfargument name="resourcesPath" /> arguments.resourcesBasePath = "!@$%^&*()";
<cfargument name="resourcesBasePath" /> }
<cfset var beanPath = "" /> beanPath =
<cfif len(resourcesBasePath) eq 0>
<cfset arguments.resourcesBasePath = "!@$%^&*()" />
</cfif>
<cfset beanPath =
resourcesPath resourcesPath
& & "."
"." & replaceList(
&
replaceList(
replace(path, resourcesBasePath, ""), replace(path, resourcesBasePath, ""),
"/,\", "/,\",
".,." ".,."
) )
& & "."
"." & replace(
&
replace(
filename, filename,
".cfc", ".cfc",
"" ""
) );
/> beanPath = replace(beanPath, "..", ".", "ALL");
<cfset beanPath = replace(beanPath, "..", ".", "ALL") /> if (left(beanPath, 1) eq ".") {
<cfif left(beanPath, 1) eq "."> beanPath = right(beanPath, len(beanPath)-1);
<cfset beanPath = right(beanPath, len(beanPath)-1) /> }
</cfif> return beanPath;
<cfreturn beanPath /> }
</cffunction>
<cffunction name="filePathToBeanName" access="private"> private function filePathToBeanName(path, filename, basepath) {
<cfargument name="path" /> if (len(basepath) eq 0) {
<cfargument name="filename" /> arguments.basePath = "!@$%^&*()";
<cfargument name="basepath" /> }
<cfif len(basepath) eq 0> return
<cfset arguments.basePath = "!@$%^&*()" />
</cfif>
<cfreturn
replaceList( replaceList(
replace(path, basepath, ""), replace(path, basepath, ""),
"/,\", "/,\",
@@ -163,40 +142,53 @@
filename, filename,
".cfc", ".cfc",
"" ""
) );
/> }
</cffunction>
<cffunction name="_recurse_ResolveDependencies" access="private"> private function _recurse_ResolveDependencies(required bean, required struct metaData) {
<cfargument name="bean" required="true" /> var local = {};
<cfargument name="metaData" type="struct" required="true" /> if (structKeyExists(arguments.metaData, "functions") and isArray(arguments.metaData.functions)) {
<cfset var local = structNew() /> for (local.f = 1; local.f <= arrayLen(arguments.metaData.functions); local.f++) {
<cfif structKeyExists(arguments.metaData, "functions") and isArray(arguments.metaData.functions)> local.fname = arguments.metaData.functions[local.f].name;
<cfloop from="1" to="#arrayLen(arguments.metaData.functions)#" index="local.f"> if (len(local.fname) gt 3) {
<cfset local.fname = arguments.metaData.functions[local.f].name /> local.propName = right(local.fname, len(local.fname)-3);
<cfif len(local.fname) gt 3> if (left(local.fname, 3) eq "set" and beanExists(local.propName, true, true)) {
<cfset local.propName = right(local.fname, len(local.fname)-3) /> invoke(arguments.bean, local.fname, [getBean(local.propName)]);
<cfif left(local.fname, 3) eq "set" and beanExists(local.propName, true, true)> }
<cfset evaluate("arguments.bean.#local.fname#(getBean('#local.propName#'))") /> }
</cfif> }
</cfif> }
</cfloop> if (structKeyExists(arguments.metaData, "properties") and isArray(arguments.metaData.properties)) {
</cfif> for (local.p = 1; local.p <= arrayLen(arguments.metaData.properties); local.p++) {
<cfif structKeyExists(arguments.metaData, "properties") and isArray(arguments.metaData.properties)> local.propName = arguments.metaData.properties[local.p].name;
<cfloop from="1" to="#arrayLen(arguments.metaData.properties)#" index="local.p"> if (beanExists(local.propName, true, true)) {
<cfset local.propName = arguments.metaData.properties[local.p].name /> arguments.bean[local.propName] = getBean(local.propName);
<cfif beanExists(local.propName, true, true)> }
<cfset arguments.bean[local.propName] = getBean(local.propName) /> }
</cfif> }
</cfloop> if (structKeyExists(arguments.metaData, "extends") and isStruct(arguments.metaData.extends)) {
</cfif> _recurse_ResolveDependencies(arguments.bean, arguments.metaData.extends);
<cfif structKeyExists(arguments.metaData, "extends") and isStruct(arguments.metaData.extends)> }
<cfset _recurse_ResolveDependencies(arguments.bean, arguments.metaData.extends) /> }
</cfif>
</cffunction>
<!--- proxy function for CF8 compatibility --->
<cffunction name="throwError">
<cfthrow attributecollection="#arguments#" />
</cffunction>
</cfcomponent> /**
* Walks the inheritance chain to check if obj extends baseSerializer.
* Replaces isInstanceOf() which fails on Lucee when extends resolves
* to a relative path instead of a mapping-prefixed path.
*/
private boolean function isSerializerInstance(required any obj) output="false" {
if (!isObject(arguments.obj)) return false;
var meta = getMetadata(arguments.obj);
while (structKeyExists(meta, "name")) {
if (listLast(meta.name, ".") == "baseSerializer") return true;
if (!structKeyExists(meta, "extends")) break;
meta = meta.extends;
}
return false;
}
private function throwError() {
cfthrow(attributecollection=arguments);
}
}
+16 -17
View File
@@ -1,21 +1,20 @@
<cfcomponent extends="taffy.core.baseDeserializer"> component extends="baseDeserializer" {
<cffunction name="getFromJson" output="false" taffy:mime="application/json,text/json" hint="get data from json"> public function getFromJson(body) output="false" taffy_mime="application/json,text/json" hint="get data from json" {
<cfargument name="body" hint="the textual request body" /> var data = 0;
<cfset var data = 0 /> var response = {};
<cfset var response = {} />
<cfif not isJson(arguments.body)> if (!isJson(arguments.body)) {
<cfset throwError(msg="Input JSON is not well formed", statusCode="400") /> throwError(msg="Input JSON is not well formed", statusCode="400");
</cfif> }
<cfset data = deserializeJSON(arguments.body) /> data = deserializeJSON(arguments.body);
<cfif not isStruct(data)> if (!isStruct(data)) {
<cfset response['_body'] = data /> response['_body'] = data;
<cfelse> } else {
<cfset response = data /> response = data;
</cfif> }
<cfreturn response /> return response;
</cffunction> }
</cfcomponent> }
+5 -10
View File
@@ -1,12 +1,7 @@
<cfcomponent extends="taffy.core.baseSerializer" output="false" hint="Serializer that uses CFML server's json serialization functionality to return json data"> component extends="baseSerializer" output="false" hint="Serializer that uses CFML server's json serialization functionality to return json data" {
<cffunction public function getAsJson() output="false" taffy_mime="application/json;text/json" taffy_default="true" hint="serializes data as JSON" {
name="getAsJson" return rereplace(replace(serializeJSON(variables.data), chr(2), '', 'ALL'), '"\\u0002', '"', 'ALL');
output="false" }
taffy:mime="application/json;text/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn rereplace(replace(serializeJSON(variables.data), chr(2), '', 'ALL'), '"\\u0002', '"', 'ALL') />
</cffunction>
</cfcomponent> }
+297
View File
@@ -0,0 +1,297 @@
component output="false" hint="Generates an OpenAPI 3.1 document from Taffy runtime metadata." {
public struct function generate()
hint="Returns an OpenAPI 3.1 struct describing all registered resources." {
var settings = application._taffy.settings;
var endpoints = application._taffy.endpoints;
var matchOrder = application._taffy.uriMatchOrder;
var openapiCfg = structKeyExists(settings, "openapi") ? settings.openapi : {};
var mediaTypes = collectResponseMediaTypes();
var doc = {
"openapi": "3.1.0",
"info": buildInfo(settings, openapiCfg),
"paths": {}
};
var servers = buildServers(openapiCfg);
if (arrayLen(servers)) {
doc["servers"] = servers;
}
for (var i = 1; i <= arrayLen(matchOrder); i++) {
var endpoint = endpoints[matchOrder[i]];
var bean = application._taffy.factory.getBean(endpoint.beanName);
var beanMeta = getMetaData(bean);
if (hasAnyKey(beanMeta, "taffy_docs_hide,taffy:docs:hide,taffy_dashboard_hide,taffy:dashboard:hide")) {
continue;
}
var funcs = collectFunctions(beanMeta);
var pathItem = {};
var validVerbs = "get,put,post,delete,options,head,patch,trace";
for (var verb in endpoint.methods) {
if (!listFindNoCase(validVerbs, verb)) continue;
var funcName = endpoint.methods[verb];
if (!structKeyExists(funcs, funcName)) continue;
var func = funcs[funcName];
if (hasAnyKey(func, "taffy_docs_hide,taffy:docs:hide,taffy_dashboard_hide,taffy:dashboard:hide")) continue;
pathItem[lcase(verb)] = buildOperation(verb, func, endpoint, beanMeta, mediaTypes);
}
if (!structIsEmpty(pathItem)) {
doc.paths[normalizePathTemplate(endpoint.srcUri, endpoint.tokens)] = pathItem;
}
}
return doc;
}
private struct function buildInfo(required struct settings, required struct openapiCfg) {
var info = {
"title": arguments.settings.docs.APIName,
"version": arguments.settings.docs.APIVersion
};
if (structKeyExists(arguments.openapiCfg, "description")) info["description"] = arguments.openapiCfg.description;
if (structKeyExists(arguments.openapiCfg, "contact")) info["contact"] = arguments.openapiCfg.contact;
if (structKeyExists(arguments.openapiCfg, "license")) info["license"] = arguments.openapiCfg.license;
return info;
}
private array function buildServers(required struct openapiCfg) {
if (structKeyExists(arguments.openapiCfg, "servers") && isArray(arguments.openapiCfg.servers)) {
return arguments.openapiCfg.servers;
}
var host = cgi.http_host;
if (!len(host)) return [];
var isHttps = cgi.server_port == 443 || (structKeyExists(cgi, "https") && cgi.https == "on");
var scheme = isHttps ? "https" : "http";
var scriptDir = getDirectoryFromPath(cgi.script_name);
if (right(scriptDir, 1) == "/" && len(scriptDir) > 1) {
scriptDir = left(scriptDir, len(scriptDir) - 1);
}
return [ { "url": scheme & "://" & host & scriptDir } ];
}
private array function collectResponseMediaTypes()
hint="Unique MIME type strings Taffy can return, derived from registered serializers." {
var mimes = {};
for (var ext in application._taffy.settings.mimeExtensions) {
for (var part in listToArray(application._taffy.settings.mimeExtensions[ext], ";")) {
mimes[trim(part)] = true;
}
}
var out = [];
for (var m in mimes) arrayAppend(out, m);
arraySort(out, "text");
return out;
}
private struct function collectFunctions(required struct metadata)
hint="Flatten the cfc's function metadata (including inherited) keyed by name. Child overrides parent." {
var result = {};
if (structKeyExists(arguments.metadata, "extends") && arguments.metadata.extends.fullname != "taffy.core.resource") {
result = collectFunctions(arguments.metadata.extends);
}
if (!structKeyExists(arguments.metadata, "functions") || !isArray(arguments.metadata.functions)) {
return result;
}
for (var f = 1; f <= arrayLen(arguments.metadata.functions); f++) {
var func = arguments.metadata.functions[f];
if (structKeyExists(func, "access") && (func.access == "private" || func.access == "package")) continue;
result[func.name] = func;
}
return result;
}
private struct function buildOperation(
required string verb,
required struct func,
required struct endpoint,
required struct beanMeta,
required array mediaTypes
) {
var op = {
"tags": [ getResourceTag(arguments.beanMeta, arguments.endpoint.beanName) ],
"operationId": arguments.endpoint.beanName & "_" & lcase(arguments.verb)
};
if (structKeyExists(arguments.beanMeta, "taffy:docs:name")) {
op["summary"] = arguments.beanMeta["taffy:docs:name"];
} else if (structKeyExists(arguments.beanMeta, "taffy_docs_name")) {
op["summary"] = arguments.beanMeta["taffy_docs_name"];
}
if (structKeyExists(arguments.func, "hint")) op["description"] = arguments.func.hint;
var tokenLookup = {};
for (var t = 1; t <= arrayLen(arguments.endpoint.tokens); t++) {
tokenLookup[lcase(arguments.endpoint.tokens[t])] = arguments.endpoint.tokens[t];
}
var parameters = [];
var bodyParams = [];
var declaredPathTokens = {};
var params = structKeyExists(arguments.func, "parameters") ? arguments.func.parameters : [];
var isBodyVerb = listFindNoCase("post,put,patch", arguments.verb) > 0;
for (var p = 1; p <= arrayLen(params); p++) {
var param = params[p];
if (hasAnyKey(param, "taffy_docs_hide,taffy:docs:hide,taffy_dashboard_hide,taffy:dashboard:hide")) continue;
if (structKeyExists(tokenLookup, lcase(param.name))) {
// use the token's original casing so the parameter name matches the path template exactly
arrayAppend(parameters, buildParameter(param, "path", true, tokenLookup[lcase(param.name)]));
declaredPathTokens[lcase(param.name)] = true;
} else if (isBodyVerb) {
arrayAppend(bodyParams, param);
} else {
var required = structKeyExists(param, "required") && param.required;
arrayAppend(parameters, buildParameter(param, "query", required));
}
}
// backfill any path tokens the resource didn't declare as args — OpenAPI requires a parameter for every {token}
for (var t2 = 1; t2 <= arrayLen(arguments.endpoint.tokens); t2++) {
var tok = arguments.endpoint.tokens[t2];
if (!structKeyExists(declaredPathTokens, lcase(tok))) {
arrayAppend(parameters, {
"name": tok,
"in": "path",
"required": true,
"schema": { "type": "string" }
});
}
}
if (arrayLen(parameters)) op["parameters"] = parameters;
if (isBodyVerb && arrayLen(bodyParams)) op["requestBody"] = buildRequestBody(bodyParams);
op["responses"] = buildResponses(arguments.mediaTypes);
return op;
}
private struct function buildParameter(required struct param, required string location, required boolean required, string nameOverride = "") {
var out = {
"name": len(arguments.nameOverride) ? arguments.nameOverride : arguments.param.name,
"in": arguments.location,
"required": arguments.required,
"schema": paramSchema(arguments.param)
};
if (structKeyExists(arguments.param, "hint")) out["description"] = arguments.param.hint;
return out;
}
private struct function buildRequestBody(required array bodyParams) {
var properties = {};
var required = [];
for (var p = 1; p <= arrayLen(arguments.bodyParams); p++) {
var param = arguments.bodyParams[p];
var schema = paramSchema(param);
if (structKeyExists(param, "hint")) schema["description"] = param.hint;
properties[param.name] = schema;
if (structKeyExists(param, "required") && param.required) arrayAppend(required, param.name);
}
var bodySchema = { "type": "object", "properties": properties };
if (arrayLen(required)) bodySchema["required"] = required;
return {
"content": {
"application/json": { "schema": bodySchema },
"application/x-www-form-urlencoded": { "schema": bodySchema }
}
};
}
private struct function buildResponses(required array mediaTypes) {
var content = {};
for (var i = 1; i <= arrayLen(arguments.mediaTypes); i++) {
content[arguments.mediaTypes[i]] = {};
}
var ok = { "description": "Successful response" };
if (!structIsEmpty(content)) ok["content"] = content;
return { "200": ok, "default": { "description": "Unexpected error" } };
}
private struct function paramSchema(required struct param) {
var schema = cfmlTypeToSchema(structKeyExists(arguments.param, "type") ? arguments.param.type : "");
if (structKeyExists(arguments.param, "default") && len(arguments.param.default)) {
schema["default"] = arguments.param.default;
}
return schema;
}
private struct function cfmlTypeToSchema(required string type) {
switch (lcase(trim(arguments.type))) {
case "numeric": case "number": case "double": case "float":
return { "type": "number" };
case "integer":
return { "type": "integer" };
case "string":
return { "type": "string" };
case "boolean":
return { "type": "boolean" };
case "struct":
return { "type": "object" };
case "array":
return { "type": "array" };
case "date": case "datetime":
return { "type": "string", "format": "date-time" };
case "uuid": case "guid":
return { "type": "string", "format": "uuid" };
case "email":
return { "type": "string", "format": "email" };
case "url":
return { "type": "string", "format": "uri" };
case "": case "any": case "variablename":
return {};
default:
return { "type": "string" };
}
}
private string function getResourceTag(required struct beanMeta, required string beanName) {
if (structKeyExists(arguments.beanMeta, "taffy:docs:name")) return arguments.beanMeta["taffy:docs:name"];
if (structKeyExists(arguments.beanMeta, "taffy_docs_name")) return arguments.beanMeta["taffy_docs_name"];
return arguments.beanName;
}
private string function normalizePathTemplate(required string uri, required array tokens)
hint="Rebuild Taffy's URI as an OpenAPI path template: {name:regex} => {name}. Handles nested braces in token regexes." {
var out = "";
var i = 1;
var tokenIdx = 1;
var n = len(arguments.uri);
while (i <= n) {
var ch = mid(arguments.uri, i, 1);
if (ch == "{") {
// find the matching close brace, honoring nesting
var depth = 1;
var j = i + 1;
while (j <= n && depth > 0) {
var c2 = mid(arguments.uri, j, 1);
if (c2 == "{") depth++;
else if (c2 == "}") depth--;
if (depth > 0) j++;
}
if (depth == 0 && tokenIdx <= arrayLen(arguments.tokens)) {
out &= "{" & arguments.tokens[tokenIdx] & "}";
tokenIdx++;
i = j + 1;
continue;
}
}
out &= ch;
i++;
}
return out;
}
private boolean function hasAnyKey(required struct target, required string keys) {
for (var k in listToArray(arguments.keys)) {
if (structKeyExists(arguments.target, k)) return true;
}
return false;
}
}
+121 -125
View File
@@ -1,141 +1,137 @@
<cfcomponent hint="base class for taffy REST components"> component hint="base class for taffy REST components" {
<cffunction name="forceString"> public function forceString(required data) hint="the data that is being forced to serialize as a string" {
<cfargument name="data" required="true" hint="the data that is being forced to serialize as a string" /> return chr(2) & arguments.data;
<cfreturn chr(2) & arguments.data /> }
</cffunction>
<cfset variables.encode = structNew() /> variables.encode = {};
<cfset variables.encode.string = forceString /> variables.encode.string = forceString;
<!--- helper functions ---> public function representationOf(required data) output="false" hint="returns an object capable of serializing the data in a variety of formats" {
<cffunction name="representationOf" access="public" output="false" hint="returns an object capable of serializing the data in a variety of formats"> return getRepInstance().setData(arguments.data);
<cfargument name="data" required="true" hint="any simple or complex data that should be returned for the request" /> }
<cfreturn getRepInstance().setData(arguments.data) />
</cffunction>
<cffunction name="rep" access="public" output="false" hint="alias for representationOf"> public function rep(required data) output="false" hint="alias for representationOf" {
<cfargument name="data" required="true" /> return representationOf(arguments.data);
<cfreturn representationOf(arguments.data) /> }
</cffunction>
<cffunction name="noData" access="private" output="false" hint="use this function to return only headers to the consumer, no data"> private function noData() output="false" hint="use this function to return only headers to the consumer, no data" {
<cfreturn getRepInstance().noData() /> return getRepInstance().noData();
</cffunction> }
<cffunction name="noContent" access="private" output="false" hint="use this function to return only headers to the consumer, no data"> private function noContent() output="false" hint="use this function to return only headers to the consumer, no data" {
<cfreturn getRepInstance().noContent() /> return getRepInstance().noContent();
</cffunction> }
<cffunction name="streamFile" access="private" output="false" hint="Use this function to specify a file name (eg c:\tmp\kitten.jpg) to be streamed to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type."> private function streamFile(required fileName) output="false" hint="Use this function to specify a file name (eg c:\tmp\kitten.jpg) to be streamed to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type." {
<cfargument name="fileName" required="true" hint="fully qualified file path (eg c:\tmp\kitten.jpg)" /> return getRepInstance().setFileName(arguments.fileName);
<cfreturn getRepInstance().setFileName(arguments.fileName) /> }
</cffunction>
<cffunction name="streamBinary" access="private" output="false" hint="Use this function to stream binary data, like a generated PDF object, to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type."> private function streamBinary(required binaryData) output="false" hint="Use this function to stream binary data, like a generated PDF object, to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type." {
<cfargument name="binaryData" required="true" hint="binary file data (eg a PDF object) that you want to return to the client" /> return getRepInstance().setFileData(arguments.binaryData);
<cfreturn getRepInstance().setFileData(arguments.binaryData) /> }
</cffunction>
<cffunction name="streamImage" access="private" output="false" hint="Use this function to stream binary data, like a generated PDF object, to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type."> private function streamImage(required binaryData) output="false" hint="Use this function to stream binary data, like a generated PDF object, to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type." {
<cfargument name="binaryData" required="true" hint="binary file data (eg a PDF object or image data) that you want to return to the client" /> return getRepInstance().setImageData(arguments.binaryData);
<cfreturn getRepInstance().setImageData(arguments.binaryData) /> }
</cffunction>
<cffunction name="saveLog"> public function saveLog(exception) {
<cfargument name="exception" /> var logger = createObject("component", application._taffy.settings.exceptionLogAdapter).init(
<cfset logger = createObject("component", application._taffy.settings.exceptionLogAdapter).init( application._taffy.settings.exceptionLogAdapterConfig
application._taffy.settings.exceptionLogAdapterConfig );
) /> logger.saveLog(exception);
<cfset logger.saveLog(exception) /> }
</cffunction>
<cffunction name="queryToArray" access="private" returntype="array" output="false"> private array function qToArray(required query q, cb) output="false" {
<cfargument name="q" type="query" required="yes" /> var local = {};
<cfargument name="cb" type="any" required="no" /> if (structKeyExists(server, "lucee")) {
<cfscript> local.Columns = listToArray(arguments.q.getColumnList(false));
var local = {}; } else {
if (structKeyExists(server, "railo") or structKeyExists(server, "lucee")) { local.Columns = arguments.q.getMetaData().getColumnLabels();
local.Columns = listToArray(arguments.q.getColumnList(false)); }
} local.QueryArray = [];
else { for (local.RowIndex = 1; local.RowIndex <= arguments.q.RecordCount; local.RowIndex++) {
local.Columns = arguments.q.getMetaData().getColumnLabels(); local.Row = {};
} local.numCols = arrayLen(local.Columns);
local.QueryArray = ArrayNew(1); for (local.ColumnIndex = 1; local.ColumnIndex <= local.numCols; local.ColumnIndex++) {
for (local.RowIndex = 1; local.RowIndex <= arguments.q.RecordCount; local.RowIndex++){ local.ColumnName = local.Columns[local.ColumnIndex];
local.Row = {}; if (local.ColumnName != "") {
local.numCols = ArrayLen( local.Columns ); local.Row[local.ColumnName] = arguments.q[local.ColumnName][local.RowIndex];
for (local.ColumnIndex = 1; local.ColumnIndex <= local.numCols; local.ColumnIndex++){
local.ColumnName = local.Columns[ local.ColumnIndex ];
if( local.ColumnName NEQ "" ) {
local.Row[ local.ColumnName ] = arguments.q[ local.ColumnName ][ local.RowIndex ];
}
}
if ( structKeyExists( arguments, "cb" ) ) {
local.Row = cb( local.Row );
}
ArrayAppend( local.QueryArray, local.Row );
}
return( local.QueryArray );
</cfscript>
</cffunction>
<cffunction name="queryToStruct" access="private" returntype="struct" output="false">
<cfargument name="q" type="query" required="yes" />
<cfargument name="cb" type="any" required="no" />
<cfset var local = {} />
<cfif q.recordcount gt 1>
<cfthrow message="Unable to convert query resultset with more than one record to a simple struct, use queryToArray() instead" />
</cfif>
<cfscript>
if (structKeyExists(server, "railo") or structKeyExists(server, "lucee")) {
local.Columns = listToArray(arguments.q.getColumnList(false));
}
else {
local.Columns = arguments.q.getMetaData().getColumnLabels();
}
local.QueryStruct = {};
local.numCols = ArrayLen( local.Columns );
for (local.ColumnIndex = 1; local.ColumnIndex <= local.numCols; local.ColumnIndex++){
local.ColumnName = local.Columns[ local.ColumnIndex ];
if( local.ColumnName NEQ "" ) {
if ( structKeyExists( arguments, "cb" ) ) {
local.QueryStruct[ local.ColumnName ] = cb( local.ColumnName, arguments.q[ local.ColumnName ][1] );
} else {
local.QueryStruct[ local.ColumnName ] = arguments.q[ local.ColumnName ][1];
}
} }
} }
if (structKeyExists(arguments, "cb")) {
local.Row = cb(local.Row);
}
arrayAppend(local.QueryArray, local.Row);
}
return local.QueryArray;
}
return( local.QueryStruct ); variables.funcs = getFunctionList();
</cfscript> if (!structKeyExists(variables.funcs, "queryToArray")) {
</cffunction> private array function queryToArray(required query q, cb) output="false" {
return qToArray(arguments.q, arguments.cb);
}
}
<!--- private struct function qToStruct(required query q, cb) output="false" {
function that gets the representation class instance var local = {};
-- if the argument is blank, we use the default from taffy settings
-- if the argument is a beanName, the bean is returned from the factory;
-- otherwise it is assumed to be a cfc path and that cfc instance is returned
--->
<cffunction name="getRepInstance" access="private" output="false">
<cfargument name="repClass" type="string" default="" />
<cfif repClass eq "">
<!--- recursion not the most efficient path here, but it's damn readable --->
<cfreturn getRepInstance(application._taffy.settings.serializer) />
<cfelseif application._taffy.factory.containsBean(arguments.repClass)>
<cfreturn application._taffy.factory.getBean(arguments.repClass) />
<cfelse>
<cfreturn createObject("component", arguments.repClass) />
</cfif>
</cffunction>
<cffunction name="addDebugData" access="package" output="false">
<cfargument name="data" type="any" />
<cfset request.debugData = arguments.data />
</cffunction>
</cfcomponent> if (q.recordcount gt 1) {
throw(message="Unable to convert query resultset with more than one record to a simple struct, use queryToArray() instead");
}
if (structKeyExists(server, "lucee")) {
local.Columns = listToArray(arguments.q.getColumnList(false));
} else {
local.Columns = arguments.q.getMetaData().getColumnLabels();
}
local.QueryStruct = {};
local.numCols = arrayLen(local.Columns);
for (local.ColumnIndex = 1; local.ColumnIndex <= local.numCols; local.ColumnIndex++) {
local.ColumnName = local.Columns[local.ColumnIndex];
if (local.ColumnName != "") {
if (structKeyExists(arguments, "cb")) {
local.QueryStruct[local.ColumnName] = cb(local.ColumnName, arguments.q[local.ColumnName][1]);
} else {
local.QueryStruct[local.ColumnName] = arguments.q[local.ColumnName][1];
}
}
}
return local.QueryStruct;
}
if (!structKeyExists(variables.funcs, "queryToStruct")) {
private struct function queryToStruct(required query q, cb) output="false" {
return qToStruct(arguments.q, arguments.cb);
}
}
/**
* function that gets the representation class instance
* -- if the argument is blank, we use the default from taffy settings
* -- if the argument is a beanName, the bean is returned from the factory;
* -- otherwise it is assumed to be a cfc path and that cfc instance is returned
*/
private function getRepInstance(string repClass="") output="false" {
var rep = "";
if (repClass eq "") {
// recursion not the most efficient path here, but it's damn readable
return getRepInstance(application._taffy.settings.serializer);
} else if (application._taffy.factory.containsBean(arguments.repClass)) {
rep = application._taffy.factory.getBean(arguments.repClass);
} else {
rep = createObject("component", arguments.repClass);
}
rep.setNoDataSends204NoContent(application._taffy.settings.noDataSends204NoContent);
return rep;
}
package function addDebugData(data) output="false" {
request.debugData = arguments.data;
}
}
+2 -10
View File
@@ -7,20 +7,12 @@
<cfswitch expression="#url.a#"> <cfswitch expression="#url.a#">
<cfcase value="jquery.min.js">
<cfcontent type="text/javascript" file="#expandPath('./jquery.min.js')#" reset="true" /><cfabort />
</cfcase>
<cfcase value="bootstrap.min.js">
<cfcontent type="text/javascript" file="#expandPath('./bootstrap.min.js')#" reset="true" /><cfabort />
</cfcase>
<cfcase value="dash.js"> <cfcase value="dash.js">
<cfcontent type="text/javascript" file="#expandPath('./dash.js')#" reset="true" /><cfabort /> <cfcontent type="text/javascript" file="#expandPath('./dash.js')#" reset="true" /><cfabort />
</cfcase> </cfcase>
<cfcase value="dash.css"> <cfcase value="dashboard.css">
<cfcontent type="text/css" file="#expandPath('./dash.css')#" reset="true" /><cfabort /> <cfcontent type="text/css" file="#expandPath('./dashboard.css')#" reset="true" /><cfabort />
</cfcase> </cfcase>
<cfcase value="loading.gif"> <cfcase value="loading.gif">
-6
View File
@@ -1,6 +0,0 @@
/**
* bootstrap.js v3.0.0 by @fat and @mdo
* Copyright 2013 Twitter Inc.
* http://www.apache.org/licenses/LICENSE-2.0
*/
if(!jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(window.jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(window.jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i<h.length-1&&i++,~i||(i=0),h.eq(i).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("dropdown");d||c.data("dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu]",f.prototype.keydown)}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.load(this.options.remote)};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show(),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(window.jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h<o?"right":d,c.removeClass(k).addClass(d)}var p=this.getCalculatedOffset(d,g,h,i);this.applyPlacement(p,d),this.$element.trigger("shown.bs."+this.type)}},b.prototype.applyPlacement=function(a,b){var c,d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),a.top=a.top+g,a.left=a.left+h,d.offset(a).addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;if("top"==b&&j!=f&&(c=!0,a.top=a.top+f-j),/bottom|top/.test(b)){var k=0;a.left<0&&(k=-2*a.left,a.left=0,d.offset(a),i=d[0].offsetWidth,j=d[0].offsetHeight),this.replaceArrow(k-e+i,i,"left")}else this.replaceArrow(j-f,j,"top");c&&d.offset(a)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach()}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.$element.trigger("hidden.bs."+this.type),this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(window.jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+514 -218
View File
@@ -4,223 +4,460 @@ if (!String.prototype.trim) {
}; };
} }
$(function(){ document.addEventListener('DOMContentLoaded', function(){
//hide request body form field for GET/DELETE on accordion open // hljs setup (if available)
$("#resources").on('shown.bs.collapse', function(e){ if (typeof hljs !== 'undefined') {
var resource = $("#resources .in .resource"); hljs.configure({ ignoreUnescapedHTML: true });
var method = resource.find('.reqMethod option:checked').html();
if (method === 'GET' || method === 'DELETE'){ // Format JSON in sample responses
resource.find('.reqBody').hide('fast'); document.querySelectorAll('.json-format').forEach(function(el) {
resource.find('.queryParams').addClass('active'); try {
}else{ var json = JSON.parse(el.textContent);
var args = window.taffy.resources[resource.data('beanName')][method.toLowerCase()]; el.textContent = JSON.stringify(json, null, 3);
var ta = resource.find('.reqBody').show('fast').find('textarea'); } catch(e) {}
ta.val(JSON.stringify(args, null, 3)); });
resource.find('.queryParams').removeClass('active');
} hljs.highlightAll();
}); }
//hide request body form field for GET/DELETE on method change
$(".resource .reqMethod").on('change', function(){ // Modal handling (native dialog)
var resource = $(this).closest('.resource'); document.querySelectorAll('[data-modal-target]').forEach(function(el) {
var method = resource.find('.reqMethod option:checked').html(); el.addEventListener('click', function() {
if (method === 'GET' || method === 'DELETE' || method == 'OPTIONS'){ var modalId = this.dataset.modalTarget;
resource.find('.reqBody').hide('fast'); document.getElementById(modalId).showModal();
}else{ });
var args = window.taffy.resources[resource.data('beanName')][method.toLowerCase()];
var ta = resource.find('.reqBody').show('fast').find('textarea');
ta.val(JSON.stringify(args, null, 3));
resource.find('.queryParams').find('input').val('');
}
}); });
$(".addParam").click(function(){ document.querySelectorAll('.modal .modal-close').forEach(function(el) {
var resource = $(this).closest('.resource') el.addEventListener('click', function() {
,params = resource.find('.queryParams'); this.closest('.modal').close();
var tmpl = '<div class="qparam row form-group"><div class="col-md-4"><input class="form-control input-small paramName" /></div><div class="col-md-1 micro">=</div><div class="col-md-4"><input class="form-control input-small paramValue" /></div><div class="col-md-2"><button class="btn delParam" tabindex="-1">-</button></div></div>'; });
params.append(tmpl);
}); });
$(".resource").on('click', '.delParam', function(){ // Close modal on backdrop click
var row = $(this).closest('.row'); document.querySelectorAll('.modal').forEach(function(modal) {
row.remove(); modal.addEventListener('click', function(e) {
if (e.target === this) {
this.close();
}
});
});
// Resource accordion
document.querySelectorAll('.resource-header').forEach(function(el) {
el.addEventListener('click', function() {
var targetId = this.dataset.target;
var content = document.getElementById(targetId);
var wasOpen = content.classList.contains('open');
content.classList.toggle('open');
// On open, show/hide request body based on method
if (!wasOpen) {
var resource = content.querySelector('.resource');
var method = resource.querySelector('.reqMethod option:checked').textContent;
if (method === 'GET' || method === 'DELETE' || method === 'OPTIONS') {
resource.querySelector('.reqBody').style.display = 'none';
resource.querySelector('.queryParams').classList.add('active');
} else {
var args = window.taffy.resources[resource.dataset.beanName];
if (args && args[method.toLowerCase()]) {
var reqBody = resource.querySelector('.reqBody');
reqBody.style.display = '';
var ta = reqBody.querySelector('textarea');
ta.value = JSON.stringify(args[method.toLowerCase()], null, 3);
} else {
resource.querySelector('.reqBody').style.display = '';
}
resource.querySelector('.queryParams').classList.remove('active');
}
}
});
});
// Doc accordion
document.querySelectorAll('.doc-accordion-trigger').forEach(function(el) {
el.addEventListener('click', function() {
var targetId = this.dataset.target;
document.getElementById(targetId).classList.toggle('open');
this.classList.toggle('open');
});
});
// Tab handling
document.querySelectorAll('.tab-btn').forEach(function(el) {
el.addEventListener('click', function() {
var tabGroup = this.closest('.tabs');
var targetId = this.dataset.tab;
tabGroup.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.classList.remove('active');
});
this.classList.add('active');
tabGroup.querySelectorAll('.tab-pane').forEach(function(pane) {
pane.classList.remove('active');
});
document.getElementById(targetId).classList.add('active');
});
});
// Reload button
var reloadBtn = document.getElementById('reload');
if (reloadBtn && window.taffy && window.taffy.config) {
reloadBtn.addEventListener('click', function() {
var cfg = window.taffy.config;
var reloadUrl = cfg.scriptName + '?dashboard&' + cfg.reloadKey + '=' + cfg.reloadPassword;
var btn = this;
btn.textContent = 'Reloading...';
btn.disabled = true;
fetch(reloadUrl)
.then(function(response) {
if (response.ok) {
document.getElementById('alerts').insertAdjacentHTML('beforeend', '<div id="reloadSuccess" class="alert alert-success">API Cache Successfully Reloaded. Refresh to see changes.</div>');
btn.disabled = false;
btn.textContent = 'Reload API Cache';
setTimeout(function() {
var el = document.getElementById('reloadSuccess');
if (el) el.remove();
}, 2000);
} else {
throw new Error('Reload failed');
}
})
.catch(function() {
document.getElementById('alerts').insertAdjacentHTML('beforeend', '<div id="reloadFail" class="alert alert-danger">API Cache Reload Failed!</div>');
btn.disabled = false;
btn.textContent = 'Reload API Cache';
setTimeout(function() {
var el = document.getElementById('reloadFail');
if (el) el.remove();
}, 2000);
});
});
}
// Resource search
var searchInput = document.getElementById('resourceSearch');
if (searchInput) {
searchInput.addEventListener('keyup', filterResources);
searchInput.addEventListener('keydown', function(e) {
if (e.keyCode == 27) {
this.value = '';
filterResources();
}
});
}
// Resource sorting
initSortPickers();
// Request body visibility on method change
document.querySelectorAll(".resource .reqMethod").forEach(function(el) {
el.addEventListener('change', function(){
var resource = this.closest('.resource');
var method = resource.querySelector('.reqMethod option:checked').textContent;
if (method === 'GET' || method === 'DELETE' || method == 'OPTIONS'){
resource.querySelector('.reqBody').style.display = 'none';
}else{
var args = window.taffy.resources[resource.dataset.beanName][method.toLowerCase()];
var reqBody = resource.querySelector('.reqBody');
reqBody.style.display = '';
var ta = reqBody.querySelector('textarea');
ta.value = JSON.stringify(args, null, 3);
resource.querySelectorAll('.queryParams input').forEach(function(inp) {
inp.value = '';
});
}
});
});
document.querySelectorAll(".addParam").forEach(function(el) {
el.addEventListener('click', function(){
var resource = this.closest('.resource');
var params = resource.querySelector('.queryParams');
var tmpl = '<div class="qparam"><input class="form-input paramName" placeholder="name" /><span class="text-muted">=</span><input class="form-input paramValue" placeholder="value" /><button class="btn btn-ghost delParam" tabindex="-1">-</button></div>';
params.insertAdjacentHTML('beforeend', tmpl);
});
});
document.querySelectorAll(".resource").forEach(function(resource) {
resource.addEventListener('click', function(e){
if (e.target.classList.contains('delParam')) {
var row = e.target.closest('.qparam');
row.remove();
}
});
}); });
//interpolate resource uri token values as they're typed //interpolate resource uri token values as they're typed
$(".resource").on('keyup', 'input', function(e){ document.querySelectorAll(".resource").forEach(function(resource) {
var $this = $(this) resource.addEventListener('keyup', function(e){
,resource = $this.closest('.resource') if (!e.target.matches('input')) return;
,tokens = params( resource.find('.reqTokens form').serialize() ) var form = resource.querySelector('.reqTokens form');
,q = qParams(resource) var tokens = form ? params(new FormData(form)) : {};
,uri = resource.data('uri') var q = qParams(resource);
var uri = resource.dataset.uri;
for (var t in tokens){ for (var t in tokens){
if (tokens[t] === '') if (tokens[t] === '')
delete tokens[t]; delete tokens[t];
} }
var result = uri.supplant(tokens); var result = uri.supplant(tokens);
result += (q.length) ? '?' + q : ''; result += (q.length) ? '?' + q : '';
resource.find('.resourceUri').val(result); resource.querySelector('.resourceUri').value = result;
});
}); });
$(".submitRequest").click(function(){ document.querySelectorAll(".submitRequest").forEach(function(el) {
var submit = $(this) el.addEventListener('click', function(){
,resource = submit.closest('.resource') var submit = this;
,reset = resource.find('.resetRequest') var resource = submit.closest('.resource');
,loading = resource.find('.progress') var reset = resource.querySelector('.resetRequest');
,response = resource.find('.response') var loading = resource.querySelector('.progress');
,basicAuth = resource.find(".basicAuth"); var response = resource.querySelector('.response');
var basicAuth = resource.querySelector(".basicAuth");
//validate tokens //validate tokens
resource.find('.has-error').removeClass('has-error'); resource.querySelectorAll('.has-error').forEach(function(el) {
var tokenErrors = resource.find('.tokenErrors'); el.classList.remove('has-error');
var tokens = resource.find('.reqTokens input'); });
tokenErrors.empty(); var tokenErrors = resource.querySelector('.tokenErrors');
for (var t=0;t<tokens.length;t++){ var tokens = resource.querySelectorAll('.reqTokens input');
var tok = $(tokens[t]); if (tokenErrors) tokenErrors.innerHTML = '';
if (tok.val().length === 0){ for (var t=0;t<tokens.length;t++){
tok.closest('.form-group').addClass('has-error').focus(); var tok = tokens[t];
tokenErrors.append('<div class="alert alert-danger">' + tok.attr('name') + ' is required</div>'); if (tok.value.length === 0){
tok.closest('.token-row').classList.add('has-error');
tok.focus();
if (tokenErrors) tokenErrors.insertAdjacentHTML('beforeend', '<div class="alert alert-danger">' + tok.name + ' is required</div>');
}
} }
} if (resource.querySelectorAll('.reqTokens .has-error').length > 0){
if (resource.find('.reqTokens .has-error').length > 0){ return false;
return false;
}
loading.show();
submit.attr('disabled','disabled');
response.hide();
//interpolate the full request path
var uri = resource.data('uri')
,form = params( resource.find('.reqTokens form').serialize() )
,path = uri.supplant(form);
var verb = resource.find('.reqMethod option:checked').val();
var body = (verb === 'GET' || verb === 'DELETE') ? qParams(resource) : resource.find('.reqBody textarea').val();
var reqHeaders = resource.find('.requestHeaders').val().replace(/\r/g, '').split('\n');
var headers = {
Accept: resource.find('.reqFormat option:checked').val()
,"Content-Type": (verb === 'GET' || verb === 'DELETE') ? "application/x-www-form-urlencoded" : "application/json"
};
for (var h in reqHeaders){
var kv = reqHeaders[h].trim().split(':');
if (kv[0].trim().length == 0){
continue;
} }
if (kv.length == 2){
headers[ kv[0].trim() ] = kv[1].trim();
}else if (kv.length == 1){
headers [kv[0].trim() ] = "";
}else{
var k = kv.shift().trim();
var v = kv.join(':').trim();
headers[ k ] = v;
}
}
var basicAuthUsername = basicAuth.find("input[name=username]").val(); loading.classList.add('show');
var basicAuthPassword = basicAuth.find("input[name=password]").val(); submit.disabled = true;
if(basicAuthUsername.length && basicAuthPassword.length){ response.classList.remove('show');
headers["Authorization"] = "Basic " + Base64.encode(basicAuthUsername + ":" + basicAuthPassword);
}
submitRequest(verb, path, headers, body, function(timeSpent, status, headers, body){ //interpolate the full request path
loading.hide(); var uri = resource.dataset.uri;
submit.removeAttr('disabled'); var form = resource.querySelector('.reqTokens form');
reset.show(); var formParams = form ? params(new FormData(form)) : {};
headers = parseHeaders(headers); var path = uri.supplant(formParams);
if (headers['content-type'].indexOf('application/json') > -1 || headers['content-type'].indexOf('text/json') > -1 || headers['content-type'].indexOf('application/vnd.api+json') > -1){ var verb = resource.querySelector('.reqMethod option:checked').value;
//indentation! var q = qParams(resource);
if (body.length){ var body = (verb === 'GET' || verb === 'DELETE') ? null : resource.querySelector('.reqBody textarea').value;
body = JSON.stringify(JSON.parse(body), null, 3); if (q.length) path += '?' + q;
// only do syntax highlighting if hljs is defined var reqHeaders = resource.querySelector('.requestHeaders').value.replace(/\r/g, '').split('\n');
if (typeof hljs === 'undefined') { var headers = {
body = body.split('\n') Accept: resource.querySelector('.reqFormat option:checked').value
.join('<br/>') ,"Content-Type": (verb === 'GET' || verb === 'DELETE') ? "application/x-www-form-urlencoded" : "application/json"
.replace(/\s/g,'&nbsp;'); };
} else { for (var h in reqHeaders){
// syntax highlight json and then replace spaces at the start of each line (or after <br/>) with &nbsp; var kv = reqHeaders[h].trim().split(':');
body = hljs.highlight("json", body).value; if (kv[0].trim().length == 0){
body = body.split('\n') continue;
.join('<br/>') }
.replace(/(\<br\/\>)(\s+)/g, function(match, p1, p2, offset, string){ if (kv.length == 2){
return [p1, p2.replace(/\s/g,'&nbsp;')].join(''); headers[ kv[0].trim() ] = kv[1].trim();
}); }else if (kv.length == 1){
} headers [kv[0].trim() ] = "";
}else{
var k = kv.shift().trim();
var v = kv.join(':').trim();
headers[ k ] = v;
} }
} }
var headerRow = response.find('.responseHeaders'); var basicAuthUsername = basicAuth.querySelector("input[name=username]").value;
headerRow.empty(); var basicAuthPassword = basicAuth.querySelector("input[name=password]").value;
response.show();
var sortable = []; if(basicAuthUsername.length && basicAuthPassword.length){
for (var h in headers){ headers["Authorization"] = "Basic " + Base64.encode(basicAuthUsername + ":" + basicAuthPassword);
sortable.push(h);
}
sortable.sort();
for (var h in sortable){
headerRow.append('<div class="row"><div class="col-md-5 headerName">' + sortable[h] + ':</div><div class="col-md-7 headerVal">' + headers[sortable[h]] + '</div></div>');
} }
response.find('.responseTime').html('Request took ' + timeSpent + 'ms'); submitRequest(verb, path, headers, body, function(timeSpent, status, headers, body){
response.find('.responseStatus').html(status); loading.classList.remove('show');
response.find('.responseBody').html(body); submit.disabled = false;
reset.style.display = '';
headers = parseHeaders(headers);
if (headers['content-type'] && (headers['content-type'].indexOf('application/json') > -1 || headers['content-type'].indexOf('text/json') > -1 || headers['content-type'].indexOf('application/vnd.api+json') > -1)){
//indentation!
if (body.length){
body = JSON.stringify(JSON.parse(body), null, 3);
// only do syntax highlighting if hljs is defined
if (typeof hljs === 'undefined') {
body = body.split('\n')
.join('<br/>')
.replace(/\s/g,'&nbsp;');
} else {
// syntax highlight json and then replace spaces at the start of each line (or after <br/>) with &nbsp;
body = hljs.highlight(body, {language: "json"}).value;
body = body.split('\n')
.join('<br/>')
.replace(/(\<br\/\>)(\s+)/g, function(match, p1, p2, offset, string){
return [p1, p2.replace(/\s/g,'&nbsp;')].join('');
});
}
}
}
var headerRow = response.querySelector('.response-headers');
headerRow.innerHTML = '';
response.classList.add('show');
var sortable = [];
for (var h in headers){
sortable.push(h);
}
sortable.sort();
for (var h in sortable){
headerRow.insertAdjacentHTML('beforeend', '<div><strong>' + sortable[h] + ':</strong> ' + headers[sortable[h]] + '</div>');
}
response.querySelector('.response-time').innerHTML = 'Request took ' + timeSpent + 'ms';
response.querySelector('.response-status').innerHTML = status;
response.querySelector('.responseBody').innerHTML = body;
});
}); });
}); });
$(".resetRequest").click(function(){ document.querySelectorAll(".resetRequest").forEach(function(el) {
var reset = $(this) el.addEventListener('click', function(){
,resource = reset.closest('.resource') var reset = this;
,response = resource.find('.response') var resource = reset.closest('.resource');
,tokens = resource.find('.reqTokens form input') var response = resource.querySelector('.response');
,params = resource.find('.queryParams input') var tokens = resource.querySelectorAll('.reqTokens form input');
,uri = resource.data('uri'); var paramInputs = resource.querySelectorAll('.queryParams input');
var uri = resource.dataset.uri;
response.hide(); response.classList.remove('show');
reset.hide(); reset.style.display = 'none';
resource.find('.resourceUri').val(uri); resource.querySelector('.resourceUri').value = uri;
tokens.each(function(){ tokens.forEach(function(inp){
$(this).val(''); inp.value = '';
});
paramInputs.forEach(function(inp){
inp.value = '';
});
}); });
params.each(function(){
$(this).val('');
})
}); });
}); });
function qParams(resource){ function qParams(resource){
var validParams = []; var validParams = [];
resource.find('.qparam').each(function(){ resource.querySelectorAll('.qparam').forEach(function(el){
var $this = $(this), n = $this.find('.paramName'), v = $this.find('.paramValue'); var n = el.querySelector('.paramName'), v = el.querySelector('.paramValue');
var nameLen = n.val().length, valLen = v.val().length; var nameLen = n.value.length, valLen = v.value.length;
if (nameLen && valLen){ if (nameLen && valLen){
validParams.push(encodeURIComponent(n.val()) + '=' + encodeURIComponent(v.val())); validParams.push(encodeURIComponent(n.value) + '=' + encodeURIComponent(v.value));
$this.removeClass('has-error'); el.classList.remove('has-error');
}else{ }else{
$this.addClass('has-error'); el.classList.add('has-error');
} }
}); });
return validParams.join('&'); return validParams.join('&');
} }
function toggleStackTrace(id){ function toggleStackTrace(id){
console.log('toggling %s', id); var el = document.getElementById(id);
$('#' + id).toggle(); if (el) el.classList.toggle('show');
} }
function params(query){ function filterResources(){
var parameters = {}, parameter; var filter = document.getElementById('resourceSearch').value.toUpperCase();
if (query.length > 1){ document.querySelectorAll('.resource-panel').forEach(function(panel) {
query = query.split('&'); var text = panel.querySelector('.resource-name').textContent;
panel.style.display = text.toUpperCase().indexOf(filter) > -1 ? '' : 'none';
});
}
function getCookie(name) {
var nameEQ = name + '=', ca = document.cookie.split(';'), i = 0, c;
for(;i < ca.length;i++) {
c = ca[i];
while (c[0]==' ') c = c.substring(1);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length);
}
return null;
}
function submitRequest(verb, resource, headers, body, callback){
var cfg = window.taffy.config;
var url = window.location.protocol + '//' + window.location.host;
var endpointURLParam = cfg.endpointURLParam;
var endpoint = resource.split('?')[0];
var contentType = null;
if (cfg.csrfCookieName && cfg.csrfHeaderName) {
var csrfCookie = getCookie(cfg.csrfCookieName);
if (csrfCookie) {
headers[cfg.csrfHeaderName] = csrfCookie;
}
}
url += cfg.scriptName + '?' + endpointURLParam + '=' + encodeURIComponent(endpoint);
if (resource.indexOf('?') && resource.split('?')[1]) {
url += '&' + resource.split('?')[1];
}
if (body && typeof body === 'string') {
try {
JSON.parse(body);
contentType = "application/json";
} catch (e) {}
}
var before = Date.now();
var fetchOptions = {
method: verb,
headers: headers,
cache: 'no-store'
};
if (body && verb !== 'GET' && verb !== 'HEAD') {
fetchOptions.body = body;
if (contentType) {
fetchOptions.headers['Content-Type'] = contentType;
}
}
fetch(url, fetchOptions)
.then(function(response) {
return response.text().then(function(text) {
var after = Date.now(), t = after - before;
var headersStr = '';
response.headers.forEach(function(value, key) {
headersStr += key + ': ' + value + '\n';
});
callback(t, response.status + " " + response.statusText, headersStr, text);
});
})
.catch(function(error) {
var after = Date.now(), t = after - before;
callback(t, "0 Network Error", "", error.message || "Request failed");
});
}
function params(formData){
var parameters = {};
if (formData instanceof FormData) {
formData.forEach(function(value, key) {
parameters[key] = value;
});
} else if (typeof formData === 'string' && formData.length > 1) {
var query = formData.split('&');
for (var i = 0; i < query.length; i++) { for (var i = 0; i < query.length; i++) {
parameter = query[i].split("="); var parameter = query[i].split("=");
if (parameter.length === 1) { parameter[1] = ""; } if (parameter.length === 1) { parameter[1] = ""; }
parameters[decodeURIComponent(parameter[0])] = decodeURIComponent(parameter[1]); parameters[decodeURIComponent(parameter[0])] = decodeURIComponent(parameter[1]);
} }
@@ -230,6 +467,7 @@ function params(query){
function parseHeaders(h){ function parseHeaders(h){
var out = {}; var out = {};
if (!h) return out;
var chunks = h.toLowerCase().split('\n'); var chunks = h.toLowerCase().split('\n');
for (var i=0,j=chunks.length; i<j; i++){ for (var i=0,j=chunks.length; i<j; i++){
var bits = chunks[i].split(': '); var bits = chunks[i].split(': ');
@@ -396,88 +634,146 @@ function expandingFormElements(){
var parent_visible_modifier_class = '.active'; var parent_visible_modifier_class = '.active';
var nextTabIndex = 1; var nextTabIndex = 1;
$('input,select,textarea,button').not('[type=hidden]').each(function(elem){ document.querySelectorAll('input,select,textarea,button').forEach(function(elem){
var $elem = $( this ); if (elem.type === 'hidden') return;
if ( $elem.parents( invisible_parent_class ).length > 0 ){ var invisibleParent = elem.closest(invisible_parent_class);
if (invisibleParent){
//could be invisible... //could be invisible...
if ( $elem.parents( invisible_parent_class + parent_visible_modifier_class ).length > 0 ){ if (invisibleParent.classList.contains('active')){
//is visible, give it a tab index //is visible, give it a tab index
$elem.attr( 'tabindex', nextTabIndex ); elem.tabIndex = nextTabIndex;
nextTabIndex++; nextTabIndex++;
} }
}else{ }else{
$elem.attr( 'tabindex', nextTabIndex ); elem.tabIndex = nextTabIndex;
nextTabIndex++; nextTabIndex++;
} }
}); });
} }
var handleExpansion = function(e){ var handleExpansion = function(e){
var $this = $(this) var el = this;
,$target = $( $this.data('target') ) var target = document.querySelector(el.dataset.target);
,isVisible = $target.hasClass('active'); if (!target) return;
var isVisible = target.classList.contains('active');
if ( $this.is('a') ){
if (el.tagName === 'A'){
e.preventDefault(); e.preventDefault();
if (isVisible){ if (isVisible){
$target.removeClass('active'); target.classList.remove('active');
$this.closest('.hide-on-expand').show('fast'); var hideOnExpand = el.closest('.hide-on-expand');
if (hideOnExpand) hideOnExpand.style.display = '';
}else{ }else{
$target.addClass('active'); target.classList.add('active');
$this.closest('.hide-on-expand').hide('fast'); var hideOnExpand = el.closest('.hide-on-expand');
if (hideOnExpand) hideOnExpand.style.display = 'none';
} }
updateTabindexes(); updateTabindexes();
return false; return false;
}else if ( $this.is('input[type=checkbox]') ){ }else if (el.type === 'checkbox'){
if (el.checked){
if ( $this.prop('checked') ){ target.classList.add('active');
$target.addClass('active');
}else{ }else{
$target.removeClass('active'); target.classList.remove('active');
} }
}else if ( $this.is('input[type=radio]') ){ }else if (el.type === 'radio'){
var expand = el.dataset.expand && el.checked;
var expand = $this.data('expand') && $this.is(':checked'); if (expand){
target.classList.add('active');
if ( expand ){
$target.addClass('active');
}else{ }else{
$target.removeClass('active'); target.classList.remove('active');
} }
}else if ( $this.is('select') ){ }else if (el.tagName === 'SELECT'){
var selectedOption = el.options[el.selectedIndex];
var selectedOption = $this.find('option:selected'); var expand = selectedOption && selectedOption.dataset.expand;
var expand = selectedOption.data('expand'); if (expand){
target.classList.add('active');
if ( expand ){
$target.addClass('active');
}else{ }else{
$target.removeClass('active'); target.classList.remove('active');
} }
}else if ( $this.is('input[type=text]') ){ }else if (el.type === 'text'){
if (el.value.trim().length > 0){
if ( $this.val().trim().length > 0 ){ target.classList.add('active');
$target.addClass('active');
}else{ }else{
$target.removeClass('active'); target.classList.remove('active');
} }
} }
updateTabindexes(); updateTabindexes();
} }
$('a.expander').on('click', handleExpansion); document.querySelectorAll('a.expander').forEach(function(el) {
$('input.expander,select.expander').on('keyup change', handleExpansion); el.addEventListener('click', handleExpansion);
});
//initialize starting state of text fields document.querySelectorAll('input.expander,select.expander').forEach(function(el) {
$('input.expander,select.expander').each(function(){ el.addEventListener('keyup', handleExpansion);
handleExpansion.apply(this); el.addEventListener('change', handleExpansion);
// initialize starting state
handleExpansion.call(el);
}); });
} }
expandingFormElements(); expandingFormElements();
function initSortPickers() {
document.querySelectorAll('.sort-picker').forEach(function(picker) {
var storeKey = picker.dataset.storeKey;
var accordion = picker.closest('section').querySelector('#resourcesAccordion');
if (!accordion) return;
// Snapshot original (match) order
var panels = Array.prototype.slice.call(accordion.querySelectorAll('.resource-panel'));
panels.forEach(function(panel, i) {
panel.dataset.sortMatch = i;
});
function sortPanels(mode) {
var sorted = panels.slice();
if (mode === 'name') {
sorted.sort(function(a, b) {
return (a.dataset.sortName || '').localeCompare(b.dataset.sortName || '');
});
} else if (mode === 'uri') {
sorted.sort(function(a, b) {
return (a.dataset.sortUri || '').localeCompare(b.dataset.sortUri || '');
});
} else {
sorted.sort(function(a, b) {
return Number(a.dataset.sortMatch) - Number(b.dataset.sortMatch);
});
}
sorted.forEach(function(panel) {
accordion.appendChild(panel);
});
// Show/hide match order hint
var hint = picker.closest('section').querySelector('.match-order-hint');
if (hint) {
hint.style.display = mode === 'match' ? '' : 'none';
}
}
// Restore saved sort
var saved = localStorage.getItem(storeKey) || 'name';
picker.querySelectorAll('.sort-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.sort === saved);
});
sortPanels(saved);
// Click handlers
picker.querySelectorAll('.sort-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
picker.querySelectorAll('.sort-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
var mode = btn.dataset.sort;
localStorage.setItem(storeKey, mode);
sortPanels(mode);
});
});
});
}
-228
View File
@@ -1,228 +0,0 @@
@import "bootstrap.min.less";
body {
padding-bottom: 30px;
font-size: 16px;
}
.ver {
font-size: 0.9em;
font-weight: bold;
display: inline-block;
position: relative;
top: -16px;
left: 187px;
}
.masthead {
button {
float: right;
margin-left: 10px;
}
}
.stackTrace {
display: none;
}
.alert {
code {
white-space: normal;
}
}
.modal {
.label {
float: right;
}
.col-sm-6 {
margin-top: 10px;
}
dl {
margin: 0;
}
ul {
padding: 0 0 0 15px;
}
}
.runner .well.resource {
margin: 15px 0 0 0;
}
#resources {
margin-top: -20px;
.accordion-toggle {
display: block;
margin: -10px -15px;
padding: 10px 15px;
&:hover {
text-decoration: none;
background: #ddd;
}
}
.resource {
h4 {
span {
font-size: 0.6em;
}
}
}
.verb {
float: right;
margin-left: 5px;
padding-top: 4px;
margin-top: -18px;
&.label-default {
background-color: #ccc;
}
}
.showDocs {
float: right;
}
.hideDocs {
float: right;
}
.col-md-6 {
.showDocs {
display: none;
}
}
.submitRequest {
float: right;
margin-left: 10px;
}
.resetRequest {
display: none;
float: right;
margin-left: 10px;
}
.micro {
padding: 5px 0;
width: 10px;
text-align: center;
}
.reqBody {
display: none;
}
.doc {
display: block;
color: #000;
}
.doc.hint {
padding-left: 20px;
color: #717171;
}
.progress {
display: none;
margin-top: 10px;
}
.response {
display: none;
}
.responseTime {
float: right;
font-size: 0.8em;
margin-top: 5px;
margin-bottom: -2px;
}
.responseBody {
white-space: nowrap;
}
pre {
overflow-x: scroll;
width: 100%;
}
.headerName {
font-weight: bold;
text-align: right;
}
.headerName,
.headerVal {
font-size: 0.8em;
line-height: 1.6em;
}
select {
width: 85px;
display: inline-block;
}
textarea {
font-family: monospace;
font-size: 0.9em;
}
}
.reqMethod {
float: left;
}
.resourceUri {
width: 100%;
max-width: 54%;
// margin: .5em 0;
margin-left: 90px;
margin-top: 0;
font-family: monospace;
}
.response {
display: none;
label {
display: block;
text-align: right;
}
}
a.expander {
cursor: pointer;
}
.expandable {
max-height: 0;
overflow: hidden;
display: block;
margin-bottom: 0;
-webkit-transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1);
-moz-transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1);
-o-transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1);
transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1); /* easeInOutQuart */
-webkit-transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
-moz-transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
-o-transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
transition-timing-function: cubic-bezier(
0.77,
0,
0.175,
1
); /* easeInOutQuart */
&.active {
max-height: 1000px;
margin-bottom: 15px;
}
}
.tab-content {
padding-top: 10px;
}
.verbHeading {
font-size: 1.6em;
font-weight: bold;
text-transform: capitalize;
}
.inputs-wrapper {
padding-left: 20px;
padding-top: 10px;
}
.resourceUriPath {
float: right;
margin-top: -15px;
}
.queryParams .form-group:last-child {
margin-bottom: 0;
}
File diff suppressed because it is too large Load Diff
+853
View File
@@ -0,0 +1,853 @@
/* Base styles */
body {
font-family:
'Atkinson Hyperlegible',
system-ui,
-apple-system,
sans-serif;
background-color: #f3f4f6;
color: #111827;
line-height: 1.5;
}
button,
input,
select,
textarea {
font-family: inherit;
}
/* Header */
.dashboard-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 2rem;
}
.dashboard-title {
font-size: 1.875rem;
font-weight: 700;
color: #111827;
margin: 0;
}
.dashboard-version {
font-size: 0.875rem;
color: #4b5563;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.15s ease;
border: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.btn-primary {
background-color: #2563eb;
color: white;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-success {
background-color: #059669;
color: white;
}
.btn-success:hover {
background-color: #047857;
}
.btn-warning {
background-color: #d97706;
color: white;
}
.btn-warning:hover {
background-color: #b45309;
}
.btn-secondary {
background-color: white;
color: #374151;
border: 1px solid #9ca3af;
}
.btn-secondary:hover {
background-color: #f3f4f6;
}
.btn-ghost {
background-color: #e5e7eb;
color: #374151;
}
.btn-ghost:hover {
background-color: #d1d5db;
}
.btn-icon {
background-color: transparent;
padding: 0.25rem;
box-shadow: none;
line-height: 0;
}
.btn-icon svg {
display: block;
width: 1.75rem;
height: 1.75rem;
}
.has-tooltip {
position: relative;
}
.has-tooltip::after {
content: attr(data-tooltip);
position: absolute;
top: calc(100% + 0.375rem);
right: 0;
background-color: #1f2937;
color: white;
font-size: 0.8125rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
visibility: hidden;
z-index: 10;
}
.has-tooltip:hover::after,
.has-tooltip:focus-visible::after {
opacity: 1;
visibility: visible;
}
/* Form controls */
.form-input {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border: 1px solid #9ca3af;
border-radius: 0.5rem;
background-color: white;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-select {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border: 1px solid #9ca3af;
border-radius: 0.5rem;
background-color: white;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.form-select:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
textarea.form-input {
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
resize: vertical;
max-width: 100%;
box-sizing: border-box;
}
/* Request body section */
.reqBody {
max-width: 100%;
overflow: hidden;
}
/* Resource panels */
.resource-panel {
background: white;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.resource-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.15s;
}
.resource-header:hover {
background-color: #f3f4f6;
}
.resource-name {
font-weight: 500;
color: #111827;
}
.resource-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
flex: 1 1 auto;
}
.resource-uri {
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
font-size: 0.875rem;
font-weight: 700;
color: #059669;
word-break: break-all;
}
/* HTTP method badges */
.verb {
display: inline-block;
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 0.25rem;
text-transform: uppercase;
}
.verb-get {
background-color: #059669;
color: white;
}
.verb-post {
background-color: #d97706;
color: white;
}
.verb-put {
background-color: #2563eb;
color: white;
}
.verb-patch {
background-color: #2563eb;
color: white;
}
.verb-delete {
background-color: #dc2626;
color: white;
}
.verb-disabled {
background-color: #e5e7eb;
color: #6b7280;
}
/* Resource content */
.resource-content {
display: none;
border-top: 1px solid #d1d5db;
padding: 1rem;
}
.resource-content.open {
display: block;
}
/* Tabs */
.tabs-nav {
display: flex;
border-bottom: 1px solid #d1d5db;
margin-bottom: 1rem;
}
.tab-btn {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: #4b5563;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
cursor: pointer;
transition: all 0.15s;
}
.tab-btn:hover {
color: #374151;
}
.tab-btn.active {
color: #2563eb;
border-bottom-color: #2563eb;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* Runner form */
.runner-form {
background-color: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
padding: 1rem;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.04);
}
.runner-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.runner-uri {
flex: 1;
min-width: 200px;
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
}
/* Expandable sections */
.toggles {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
}
.expander {
font-size: 0.875rem;
color: #2563eb;
cursor: pointer;
}
.expander:hover {
color: #1d4ed8;
text-decoration: underline;
}
.expandable {
display: none;
background: white;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.expandable.active,
.expandable.open {
display: block;
}
.section-title {
font-size: 0.875rem;
font-weight: 600;
color: #111827;
margin-bottom: 0.75rem;
margin-top: 0;
}
.section-subtitle {
font-weight: 400;
color: #4b5563;
}
/* Query params */
.qparam {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.qparam .form-input {
flex: 1;
}
.qparam.has-error .form-input {
border-color: #dc2626;
}
/* Token inputs */
.token-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.token-row.has-error .form-input {
border-color: #dc2626;
}
.token-label {
width: 8rem;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}
.token-input {
flex: 1;
}
/* Basic auth */
.auth-row {
display: flex;
gap: 0.75rem;
}
.auth-row .form-input {
flex: 1;
}
/* Response */
.response {
display: none;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #d1d5db;
}
.response.show {
display: block;
}
.response-headers {
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
font-size: 0.75rem;
color: #4b5563;
white-space: pre-wrap;
margin-bottom: 0.5rem;
}
.response-time {
font-size: 0.875rem;
color: #4b5563;
margin-bottom: 0.5rem;
}
.response-status {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
background-color: #e5e7eb;
border-radius: 0.25rem;
margin-bottom: 0.75rem;
}
.response-body {
background-color: #1f2937;
color: #f3f4f6;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #374151;
overflow-x: auto;
font-size: 0.875rem;
margin: 0;
}
.response-body code {
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
}
.response-body code.hljs {
padding: 0;
}
.response .response-body {
padding: 1px;
}
.response .response-body .responseBody {
border-radius: 0.4rem;
padding: 1rem;
}
/* Sample response in docs - lime green theme */
.sample-response-content.doc-accordion-content {
padding: 0;
}
.sample-response-content.doc-accordion-content .response-body {
margin: 0;
padding: 1rem;
border-radius: 0;
border: 0;
background-color: #f0fdf4;
color: #14532d;
}
.sample-response-content.doc-accordion-content .response-body code.hljs {
background-color: transparent;
}
/* Progress */
.progress {
display: none;
height: 4px;
background-color: #dbeafe;
border-radius: 2px;
overflow: hidden;
margin: 1rem 0;
}
.progress.show {
display: block;
}
.progress-bar {
height: 100%;
background-color: #2563eb;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Alerts */
.alert {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.alert-info {
background-color: #eff6ff;
border: 1px solid #bfdbfe;
color: #1e40af;
}
.alert-warning {
background-color: #fffbeb;
border: 1px solid #fde68a;
color: #92400e;
}
.alert-success {
background-color: #ecfdf5;
border: 1px solid #a7f3d0;
color: #065f46;
}
.alert-danger {
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
/* Modal (native dialog) */
.modal {
border: none;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 42rem;
width: calc(100% - 2rem);
max-height: 90vh;
padding: 0;
overflow: hidden;
}
.modal::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid #d1d5db;
}
.modal-title {
font-size: 1.125rem;
font-weight: 600;
color: #111827;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: #9ca3af;
cursor: pointer;
line-height: 1;
}
.modal-close:hover {
color: #4b5563;
}
.modal-body {
overflow-y: auto;
max-height: calc(90vh - 4rem);
}
/* Config table */
.config-table {
width: 100%;
font-size: 0.875rem;
}
.config-table tr {
border-bottom: 1px solid #f3f4f6;
}
.config-table tr:hover {
background-color: #f3f4f6;
}
.config-table td {
padding: 0.75rem 1.5rem;
vertical-align: top;
}
.config-table td:first-child {
font-weight: 500;
color: #111827;
}
.config-table td:last-child {
color: #4b5563;
}
.help-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
background-color: #e5e7eb;
color: #4b5563;
font-size: 0.75rem;
border-radius: 50%;
text-decoration: none;
margin-right: 0.5rem;
}
.help-link:hover {
background-color: #d1d5db;
}
/* Documentation tab */
.doc-section {
margin: 0;
}
.doc-verb-title {
font-size: 1.125rem;
font-weight: 600;
text-transform: uppercase;
color: #111827;
margin-bottom: 0.5rem;
}
.doc-hint {
color: #4b5563;
margin-bottom: 1rem;
}
.doc-accordion {
border: 1px solid #d1d5db;
border-radius: 0.5rem;
overflow: hidden;
margin-bottom: 0.75rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.doc-accordion-trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.5rem 1rem;
background-color: #f3f4f6;
border: none;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
cursor: pointer;
text-align: left;
}
.doc-accordion-trigger:hover {
background-color: #f3f4f6;
}
.doc-accordion-trigger svg {
width: 1rem;
height: 1rem;
transition: transform 0.2s;
}
.doc-accordion-trigger.open svg {
transform: rotate(180deg);
}
.doc-accordion-content {
display: none;
padding: 1rem;
border-top: 1px solid #d1d5db;
}
.doc-accordion-content.open {
display: block;
}
.param-item {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 1px solid #e5e7eb;
}
.param-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.param-badge {
display: inline-block;
width: 4.5rem;
padding: 0.125rem 0;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
border-radius: 0.25rem;
text-align: center;
}
.param-optional {
background-color: #e5e7eb;
color: #4b5563;
}
.param-required {
background-color: #fee2e2;
color: #991b1b;
}
.param-type {
display: inline-block;
width: 4rem;
font-size: 0.75rem;
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
color: #6b7280;
background-color: #e5e7eb;
padding: 0.125rem 0;
border-radius: 0.25rem;
text-align: center;
}
.param-name {
font-weight: 600;
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
color: #111827;
}
.param-default {
font-size: 0.75rem;
color: #6b7280;
}
.param-constraint {
display: inline-block;
font-size: 0.6875rem;
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
color: #1e40af;
background-color: #dbeafe;
padding: 0.0625rem 0.375rem;
border-radius: 0.25rem;
}
.param-hint {
flex-basis: 100%;
font-size: 0.8125rem;
color: #4b5563;
margin-top: 0.25rem;
padding-left: 0;
}
/* Empty state */
.empty-state {
background-color: #fffbeb;
border: 1px solid #fde68a;
border-radius: 0.5rem;
overflow: hidden;
margin-top: 1rem;
}
.empty-state-header {
padding: 0.75rem 1rem;
background-color: #fef3c7;
font-weight: 500;
color: #92400e;
}
.empty-state-body {
padding: 1rem;
color: #92400e;
}
.empty-state-body p {
margin-bottom: 0.75rem;
}
.empty-state-body p:last-child {
margin-bottom: 0;
}
.empty-state-body a {
color: #b45309;
text-decoration: underline;
}
.empty-state-body code {
background-color: #fef3c7;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
/* Sort picker */
.sort-picker {
display: inline-flex;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
overflow: hidden;
}
.sort-btn {
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
background: white;
color: #4b5563;
border: none;
border-right: 1px solid #d1d5db;
cursor: pointer;
transition: all 0.15s;
}
.sort-btn:last-child {
border-right: none;
}
.sort-btn:hover {
background-color: #f3f4f6;
}
.sort-btn.active {
background-color: #2563eb;
color: white;
}
/* Stack trace */
.stackTrace {
display: none;
margin-top: 0.5rem;
}
.stackTrace.show {
display: block;
}
/* Utility */
.text-muted {
color: #4b5563;
}
.font-mono {
font-family: 'Atkinson Hyperlegible Mono', ui-monospace, monospace;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
margin-top: 1rem;
}
+247 -159
View File
@@ -4,8 +4,13 @@
<title><cfoutput>#application._taffy.settings.docs.APIName# Documentation - #application._taffy.settings.docs.APIversion#</cfoutput></title> <title><cfoutput>#application._taffy.settings.docs.APIName# Documentation - #application._taffy.settings.docs.APIversion#</cfoutput></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/🍬" /> <link rel="icon" href="https://fav.farm/🍬" />
<cfif application._taffy.settings.allowGoogleFonts>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;500;600;700&family=Atkinson+Hyperlegible+Mono:wght@400;600;700&display=swap" rel="stylesheet">
</cfif>
<style> <style>
<cfinclude template="dash.css" /> <cfinclude template="dashboard.css" />
<cfinclude template="highlight-github.min.css" /> <cfinclude template="highlight-github.min.css" />
</style> </style>
</head> </head>
@@ -14,15 +19,29 @@
window.taffy = { resources: {} }; window.taffy = { resources: {} };
</script> </script>
<div class="container"> <div style="max-width: 1200px; margin: 0 auto; padding: 1.5rem;">
<div class="masthead">
<h1><cfoutput>#application._taffy.settings.docs.APIName#</cfoutput></h1>
<span class="ver text-muted" style="left: 0">Version <cfoutput>#application._taffy.settings.docs.APIversion#</cfoutput></span>
</div>
<div class="row" id="resources"> <header class="dashboard-header">
<h3>Resources:</h3> <div>
<div class="panel-group" id="resourcesAccordion"> <h1 class="dashboard-title"><cfoutput>#application._taffy.settings.docs.APIName#</cfoutput></h1>
<span class="dashboard-version">Version <cfoutput>#application._taffy.settings.docs.APIversion#</cfoutput></span>
</div>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-icon has-tooltip" id="openapi" data-tooltip="OpenAPI/Swagger" aria-label="OpenAPI/Swagger" onclick="window.location.href = '<cfoutput>#CGI.SCRIPT_NAME#</cfoutput>?openapi'"><cfinclude template="swagger.svg" /></button>
</div>
</header>
<section id="resources">
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<h2 style="font-size: 1.25rem; font-weight: 600; margin: 0;">Resources:</h2>
<div class="sort-picker" data-store-key="taffyDocsSort">
<button type="button" class="sort-btn active" data-sort="name">Name</button>
<button type="button" class="sort-btn" data-sort="uri">URI</button>
<button type="button" class="sort-btn" data-sort="match">Match Order</button>
</div>
</div>
<div id="resourcesAccordion">
<cfoutput> <cfoutput>
<cfloop from="1" to="#arrayLen(application._taffy.uriMatchOrder)#" index="local.resource"> <cfloop from="1" to="#arrayLen(application._taffy.uriMatchOrder)#" index="local.resource">
<cfset local.currentResource = application._taffy.endpoints[application._taffy.uriMatchOrder[local.resource]] /> <cfset local.currentResource = application._taffy.endpoints[application._taffy.uriMatchOrder[local.resource]] />
@@ -31,159 +50,144 @@
<cfif structKeyExists(local.beanMeta, "taffy_docs_hide") OR structKeyExists(local.beanMeta, "taffy:docs:hide")> <cfif structKeyExists(local.beanMeta, "taffy_docs_hide") OR structKeyExists(local.beanMeta, "taffy:docs:hide")>
<cfscript>continue;</cfscript> <cfscript>continue;</cfscript>
</cfif> </cfif>
<div class="panel panel-default"> <cfset local.displayName = local.currentResource.beanName />
<div class="panel-heading"> <cfif structKeyExists(local.beanMeta, "taffy:docs:name")>
<h4 class="panel-title"> <cfset local.displayName = local.beanMeta['taffy:docs:name'] />
<a href="###local.currentResource.beanName#" class="accordion-toggle" data-toggle="collapse" data-parent="##resourcesAccordion"> <cfelseif structKeyExists(local.beanMeta, "taffy_docs_name")>
<cfif structKeyExists(local.beanMeta, "taffy:docs:name")> <cfset local.displayName = local.beanMeta['taffy_docs_name'] />
#local.beanMeta['taffy:docs:name']# </cfif>
<cfelseif structKeyExists(local.beanMeta, "taffy_docs_name")> <div class="resource-panel" data-sort-name="#lCase(local.displayName)#" data-sort-uri="#lCase(local.currentResource.srcUri)#">
#local.beanMeta['taffy_docs_name']# <div class="resource-header" data-target="#local.currentResource.beanName#">
<cfelse> <span class="resource-name">#local.displayName#</span>
#local.currentResource.beanName# <span class="resource-meta">
</cfif> <code class="resource-uri">#local.currentResource.srcUri#</code>
</a> <cfloop list="GET,POST,PUT,PATCH,DELETE" index="local.verb">
<cfloop list="DELETE|warning,PATCH|warning,PUT|warning,POST|danger,GET|primary" index="local.verb"> <cfif structKeyExists(local.currentResource.methods, local.verb)>
<cfif structKeyExists(local.currentResource.methods, listFirst(local.verb,'|'))> <span class="verb verb-#lcase(local.verb)#">#local.verb#</span>
<span class="verb label label-success">#ucase(listFirst(local.verb,'|'))#</span>
</cfif> </cfif>
</cfloop> </cfloop>
<code style="float:right; margin-top: -15px; display: inline-block;">#local.currentResource.srcUri#</code> </span>
</h4>
</div> </div>
<div id="#local.currentResource.beanName#" class="in"> <div class="resource-content" id="#local.currentResource.beanName#">
<div class="panel-body resourceWrapper"> <cfset local.metadata = getMetaData(application._taffy.factory.getBean(local.currentResource.beanName)) />
<div class="col-md-12 docs"> <cfset local.docData = getHintsFromMetadata(local.metadata) />
<cfset local.metadata = getMetaData(application._taffy.factory.getBean(local.currentResource.beanName)) /> <cfif structKeyExists(local.docData, 'hint')>
<cfset local.docData = getHintsFromMetadata(local.metadata) /> <p class="doc-hint">#local.docData.hint#</p>
<cfif structKeyExists(local.docData, 'hint')><div class="doc">#docData.hint#</div><hr/></cfif> </cfif>
<cfset local.found = { get=false, post=false, put=false, patch=false, delete=false } /> <cfset local.found = { get=false, post=false, put=false, patch=false, delete=false } />
<cfloop from="1" to="#arrayLen(local.docData.functions)#" index="local.f"> <cfloop from="1" to="#arrayLen(local.docData.functions)#" index="local.f">
<cfset local.func = local.docData.functions[local.f] /> <cfset local.func = local.docData.functions[local.f] />
<cfset verbs = "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD" /> <cfset verbs = "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD" />
<cfset thisVerb = local.func.name /> <cfset thisVerb = local.func.name />
<cfif structKeyExists(local.func,"taffy_verb")> <cfif structKeyExists(local.func,"taffy_verb")>
<cfset thisVerb = local.func.taffy_verb /> <cfset thisVerb = local.func.taffy_verb />
<cfelseif structKeyExists(local.func,"taffy:verb")> <cfelseif structKeyExists(local.func,"taffy:verb")>
<cfset thisVerb = local.func['taffy:verb'] /> <cfset thisVerb = local.func['taffy:verb'] />
</cfif>
<cfif listFindNoCase(verbs, thisVerb) eq 0
OR structKeyExists(local.func, "taffy_docs_hide")
OR structKeyExists(local.func, "taffy:docs:hide")>
<cfscript>continue;</cfscript>
</cfif>
<cfset local.found[local.func.name] = true />
<div class="doc-section">
<h5 class="doc-verb-title">#thisVerb#</h5>
<cfif structKeyExists(local.func, "hint")>
<p class="doc-hint">#local.func.hint#</p>
</cfif>
<!--- begin inputs --->
<cfset local.visibleParams = [] />
<cfloop from="1" to="#arrayLen(local.func.parameters)#" index="local.p">
<cfset local.param = local.func.parameters[local.p] />
<cfif NOT (structKeyExists(local.param, "taffy_docs_hide") OR structKeyExists(local.param, "taffy:docs:hide"))>
<cfset arrayAppend(local.visibleParams, local.param) />
</cfif> </cfif>
<cfif listFindNoCase(verbs, thisVerb) eq 0
OR structKeyExists(local.func, "taffy_docs_hide")
OR structKeyExists(local.func, "taffy:docs:hide")>
<cfscript>continue;</cfscript><!--- this has to be script for CF8 compat --->
</cfif>
<cfset local.found[local.func.name] = true />
<div class="col-md-12"><h5 class="verbHeading">#thisVerb#</h5></div>
<cfif structKeyExists(local.func, "hint")>
<div class="col-md-12 doc">#local.func.hint#</div>
</cfif>
<div class="inputs-wrapper">
<!--- begin inputs panel --->
<cfset inputsCount = 0 />
<cfloop from="1" to="#arrayLen(local.func.parameters)#" index="local.p">
<cfset local.param = local.func.parameters[local.p] />
<cfif structKeyExists(local.param, "taffy_docs_hide") OR structKeyExists(local.param, "taffy:docs:hide")>
<cfscript>continue;</cfscript>
</cfif>
<cfset inputsCount++ />
</cfloop>
<cfif inputsCount gt 0>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title"><a href="###local.currentResource.beanName#_#local.func.name#_inputs" class="accordion-toggle" data-toggle="collapse" data-parent="###local.currentResource.beanName#_docs">Inputs</a></h6>
</div>
<div class="panel-collapse collapse" id="#local.currentResource.beanName#_#local.func.name#_inputs">
<div class="panel-body">
<cfloop from="1" to="#arrayLen(local.func.parameters)#" index="local.p">
<cfset local.param = local.func.parameters[local.p] />
<cfif structKeyExists(local.param, "taffy_docs_hide") OR structKeyExists(local.param, "taffy:docs:hide")>
<cfscript>continue;</cfscript>
</cfif>
<div class="row">
<div class="col-md-12">
<strong>#local.param.name#</strong>
<cfif not structKeyExists(local.param, 'required') or not local.param.required>
optional
<cfelse>
required
</cfif>
<cfif structKeyExists(local.param, "type")>
#local.param.type#
</cfif>
<cfif structKeyExists(local.param, "default")>
<cfif local.param.default eq "">
(default: "")
<cfelse>
(default: #local.param.default#)
</cfif>
<cfelse>
<!--- no default value --->
</cfif>
<cfif structKeyExists(local.param, "hint")>
<br/><p class="doc hint">#local.param.hint#</p>
</cfif>
</div>
</div>
</cfloop>
</div>
</div>
</div>
</cfif>
<!--- end inputs panel --->
<!--- begin sample response --->
<cfset hasSample = false />
<cfset sample = '' />
<cfloop from="1" to="#arrayLen(local.beanMeta.functions)#" index="functionIndex">
<cfif local.beanMeta.functions[functionIndex].name eq 'sample#local.func.name#Response'>
<cfset hasSample = true />
<cfinvoke
component="#local.bean#"
method="sample#local.func.name#Response"
argumentcollection={}
returnvariable="sample"
/>
<cfbreak />
</cfif>
</cfloop>
<cfif hasSample>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a href="###local.currentResource.beanName#_#local.func.name#_sample" class="accordion-toggle" data-toggle="collapse" data-parent="###local.currentResource.beanName#_docs">Sample Response</a>
</h6>
</div>
<div class="panel-collapse collapse" id="#local.currentResource.beanName#_#local.func.name#_sample">
<div class="panel-body">
<div class="col-md-12">
<script type="text/javascript" defer>
document.write("<pre><code>");
document.write(JSON.stringify(#serializeJson(sample)#, null, ' '));
document.write("</code></pre>");
</script>
</div>
</div>
</div>
</div>
</div>
</div>
</cfif>
<!--- end sample response --->
</div>
</cfloop> </cfloop>
</div><!-- /col-md-6 (docs) --> <cfif arrayLen(local.visibleParams) gt 0>
</div> <div class="doc-accordion">
<button type="button" class="doc-accordion-trigger" data-target="#local.currentResource.beanName#_#local.func.name#_inputs">
Inputs
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="doc-accordion-content" id="#local.currentResource.beanName#_#local.func.name#_inputs">
<cfloop array="#local.visibleParams#" index="local.param">
<div class="param-item">
<cfif not structKeyExists(local.param, 'required') or not local.param.required>
<span class="param-badge param-optional">optional</span>
<cfelse>
<span class="param-badge param-required">required</span>
</cfif>
<cfif structKeyExists(local.param, "type")>
<span class="param-type">#local.param.type#</span>
</cfif>
<span class="param-name">#local.param.name#</span>
<cfif structKeyExists(local.param, "taffy_minlength")>
<span class="param-constraint">min length: #local.param["taffy_minlength"]#</span>
</cfif>
<cfif structKeyExists(local.param, "taffy_maxlength")>
<span class="param-constraint">max length: #local.param["taffy_maxlength"]#</span>
</cfif>
<cfif structKeyExists(local.param, "taffy_min")>
<span class="param-constraint">min: #local.param["taffy_min"]#</span>
</cfif>
<cfif structKeyExists(local.param, "taffy_max")>
<span class="param-constraint">max: #local.param["taffy_max"]#</span>
</cfif>
<cfif structKeyExists(local.param, "taffy_pattern")>
<span class="param-constraint">pattern: #local.param["taffy_pattern"]#</span>
</cfif>
<cfif structKeyExists(local.param, "default")>
<span class="param-default">
<cfif local.param.default eq "">(default: "")<cfelse>(default: #local.param.default#)</cfif>
</span>
</cfif>
<cfif structKeyExists(local.param, "hint")>
<p class="param-hint">#local.param.hint#</p>
</cfif>
</div>
</cfloop>
</div>
</div>
</cfif>
<!--- end inputs --->
<!--- begin sample response --->
<cfset hasSample = false />
<cfset sample = '' />
<cfloop from="1" to="#arrayLen(local.beanMeta.functions)#" index="functionIndex">
<cfif local.beanMeta.functions[functionIndex].name eq 'sample#local.func.name#Response'>
<cfset hasSample = true />
<cfinvoke
component="#local.bean#"
method="#local.beanMeta.functions[functionIndex].name#"
returnvariable="sample"
/>
<cfbreak />
</cfif>
</cfloop>
<cfif hasSample>
<div class="doc-accordion">
<button type="button" class="doc-accordion-trigger" data-target="#local.currentResource.beanName#_#local.func.name#_sample">
Sample Response
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="doc-accordion-content sample-response-content" id="#local.currentResource.beanName#_#local.func.name#_sample">
<pre class="response-body"><code class="json-format">#serializeJson(sample)#</code></pre>
</div>
</div>
</cfif>
<!--- end sample response --->
</div>
</cfloop>
</div> </div>
</div> </div>
</cfloop> </cfloop>
</cfoutput> </cfoutput>
</div><!-- /panel-group --> </div>
<br />
<cfif arrayLen(application._taffy.uriMatchOrder) eq 0> <cfif arrayLen(application._taffy.uriMatchOrder) eq 0>
<div class="panel panel-warning"> <div class="empty-state">
<div class="panel-heading">Taffy is running but you haven't defined any resources yet.</div> <div class="empty-state-header">Taffy is running but you haven't defined any resources yet.</div>
<div class="panel-body"> <div class="empty-state-body">
<p> <p>
It looks like you don't have any resources defined. Get started by creating the folder It looks like you don't have any resources defined. Get started by creating the folder
<code><cfoutput>#guessResourcesFullPath()#</cfoutput></code>, in which you should place your <code><cfoutput>#guessResourcesFullPath()#</cfoutput></code>, in which you should place your
@@ -201,19 +205,103 @@
</div> </div>
</div> </div>
</cfif> </cfif>
<div class="alert alert-info">Resources are listed in matching order. From top to bottom, the first URI to match the request is used.</div>
</div><!-- /#resources -->
</div><!-- /container --> <div class="alert alert-info mt-4 match-order-hint" style="display: none;">Resources are listed in matching order. From top to bottom, the first URI to match the request is used.</div>
</section>
</div>
<script type="text/javascript"> <script type="text/javascript">
<cfinclude template="jquery.min.js" />
<cfinclude template="bootstrap.min.js" />
<cfinclude template="highlight.min.js" /> <cfinclude template="highlight.min.js" />
<cfinclude template="dash.js" />
$(function(){ document.addEventListener('DOMContentLoaded', function(){
hljs.initHighlighting(); hljs.initHighlighting();
// Resource header toggle
document.querySelectorAll('.resource-header').forEach(function(header) {
header.addEventListener('click', function() {
var targetId = this.getAttribute('data-target');
var content = document.getElementById(targetId);
if (content) {
content.classList.toggle('open');
}
});
});
// Doc accordion toggle
document.querySelectorAll('.doc-accordion-trigger').forEach(function(trigger) {
trigger.addEventListener('click', function() {
var targetId = this.getAttribute('data-target');
var content = document.getElementById(targetId);
if (content) {
this.classList.toggle('open');
content.classList.toggle('open');
}
});
});
// Format JSON in sample responses
document.querySelectorAll('.json-format').forEach(function(el) {
try {
var data = JSON.parse(el.textContent);
el.textContent = JSON.stringify(data, null, ' ');
hljs.highlightElement(el);
} catch(e) {}
});
// Resource sorting
document.querySelectorAll('.sort-picker').forEach(function(picker) {
var storeKey = picker.dataset.storeKey;
var accordion = picker.closest('section').querySelector('#resourcesAccordion');
if (!accordion) return;
var panels = Array.prototype.slice.call(accordion.querySelectorAll('.resource-panel'));
panels.forEach(function(panel, i) {
panel.dataset.sortMatch = i;
});
function sortPanels(mode) {
var sorted = panels.slice();
if (mode === 'name') {
sorted.sort(function(a, b) {
return (a.dataset.sortName || '').localeCompare(b.dataset.sortName || '');
});
} else if (mode === 'uri') {
sorted.sort(function(a, b) {
return (a.dataset.sortUri || '').localeCompare(b.dataset.sortUri || '');
});
} else {
sorted.sort(function(a, b) {
return Number(a.dataset.sortMatch) - Number(b.dataset.sortMatch);
});
}
sorted.forEach(function(panel) {
accordion.appendChild(panel);
});
var hint = picker.closest('section').querySelector('.match-order-hint');
if (hint) {
hint.style.display = mode === 'match' ? '' : 'none';
}
}
var saved = localStorage.getItem(storeKey) || 'name';
picker.querySelectorAll('.sort-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.sort === saved);
});
sortPanels(saved);
picker.querySelectorAll('.sort-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
picker.querySelectorAll('.sort-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
var mode = btn.dataset.sort;
localStorage.setItem(storeKey, mode);
sortPanels(mode);
});
});
});
}); });
</script> </script>
</body> </body>
+9 -30
View File
@@ -1,31 +1,10 @@
/* pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
github.com style (c) Vasily Polovnyov <vast@whiteants.net> Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
highlight.js v8.2 Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
Copyright (c) 2006, Ivan Sagalaev */.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of highlight.js nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-javadoc{color:#998;font-style:italic}.hljs-keyword,.css .rule .hljs-keyword,.hljs-winutils,.javascript .hljs-title,.nginx .hljs-title,.hljs-subst,.hljs-request,.hljs-status{color:#333;font-weight:bold}.hljs-number,.hljs-hexcolor,.ruby .hljs-constant{color:#008080}.hljs-string,.hljs-tag .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula{color:#d14}.hljs-title,.hljs-id,.scss .hljs-preprocessor{color:#900;font-weight:bold}.javascript .hljs-title,.hljs-list .hljs-keyword,.hljs-subst{font-weight:normal}.hljs-class .hljs-title,.hljs-type,.vhdl .hljs-literal,.tex .hljs-command{color:#458;font-weight:bold}.hljs-tag,.hljs-tag .hljs-title,.hljs-rules .hljs-property,.django .hljs-tag .hljs-keyword{color:#000080;font-weight:normal}.hljs-attribute,.hljs-variable,.lisp .hljs-body{color:#008080}.hljs-regexp{color:#009926}.hljs-symbol,.ruby .hljs-symbol .hljs-string,.lisp .hljs-keyword,.clojure .hljs-keyword,.scheme .hljs-keyword,.tex .hljs-special,.hljs-prompt{color:#990073}.hljs-built_in{color:#0086b3}.hljs-preprocessor,.hljs-pragma,.hljs-pi,.hljs-doctype,.hljs-shebang,.hljs-cdata{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa}
+1213 -29
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,4 +0,0 @@
/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,N,e),g(f,c,O,e)):(f++,j.call(a,g(f,c,N,e),g(f,c,O,e),g(f,c,N,c.notifyWith))):(d!==N&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),
a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function $(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Z,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=W.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ba=new RegExp("^(?:([+-])=|)("+aa+")([a-z%]*)$","i"),ca=["Top","Right","Bottom","Left"],da=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=W.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&da(d)&&(e[f]=ha(d))):"none"!==c&&(e[f]="none",W.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],"globalEval",!b||W.get(b[c],"globalEval"))}var pa=/<|&#?\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ka.exec(f)||["",""])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&B(this,"input"))return this.click(),!1},_default:function(a){return B(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\s*(?:[^=]|=\s*.checked.)/i,Ca=/^true\/(.*)/,Da=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ja.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||"")&&!W.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,""),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,"script"),g.length>0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp("^("+aa+")(?!px)[a-z%]+$","i"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",ra.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:"absolute",visibility:"hidden",display:"block"},Ta={letterSpacing:"0",fontWeight:"400"},Ua=["Webkit","Moz","ms"],Va=d.createElement("div").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ca[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ca[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ca[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ca[f]+"Width",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g="border-box"===r.css(a,"boxSizing",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),"auto"===f&&(f=a["offset"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?"border":"content"),d,e)+"px")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,"string"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(j[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),"normal"===e&&b in Ta&&(e=Ta[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=ba.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,"marginLeft"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&da(a),q=W.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],cb.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=W.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ia([a],!0),j=a.style.display||j,k=r.css(a,"display"),ia([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=W.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ia([a],!0),m.done(function(){p||ia([a]),W.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=hb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],Array.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=kb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=ab||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(i||h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:ab||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);f<g;f++)if(d=kb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,hb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j}r.Animation=r.extend(kb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return fa(c.elem,a,ba.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(L);for(var c,d=0,e=a.length;d<e;d++)c=a[d],kb.tweeners[c]=kb.tweeners[c]||[],kb.tweeners[c].unshift(b)},prefilters:[ib],prefilter:function(a,b){b?kb.prefilters.unshift(a):kb.prefilters.push(a)}}),r.speed=function(a,b,c){var d=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off?d.duration=0:"number"!=typeof d.duration&&(d.duration in r.fx.speeds?d.duration=r.fx.speeds[d.duration]:d.duration=r.fx.speeds._default),null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){r.isFunction(d.old)&&d.old.call(this),d.queue&&r.dequeue(this,d.queue)},d},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(da).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=kb(this,r.extend({},a),f);(e||W.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=W.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&db.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=W.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),r.each({slideDown:gb("show"),slideUp:gb("hide"),slideToggle:gb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(ab=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),ab=void 0},r.fx.timer=function(a){r.timers.push(a),r.fx.start()},r.fx.interval=13,r.fx.start=function(){bb||(bb=!0,eb())},r.fx.stop=function(){bb=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var lb,mb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),
null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Tb=[],Ub=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Tb.pop()||r.expando+"_"+ub++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Ub.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ub.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Ub,"$1"+e):b.jsonp!==!1&&(b.url+=(vb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Tb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=pb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Vb=a.jQuery,Wb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Wb),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r});
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 0 256 256"><path d="M128 250a122 122 0 1 1 0-244 122 122 0 0 1 0 244Z" fill="#85EA2D"/><path d="M128 12a116 116 0 1 1 0 232 116 116 0 0 1 0-232m0-12a128 128 0 1 0 0 256 128 128 0 0 0 0-256Z" fill="#173647"/><path d="m81 87-1 13-1 13c-2 6-5 11-11 15 11 7 12 17 13 28v17c1 5 2 6 7 6h6v14c-14 2-25-2-27-13l-2-13v-14c-1-12-3-17-15-17v-16h3c6-1 9-3 11-9l1-11 1-22c2-10 8-15 19-15h9v14h-4c-8 0-9 3-9 10Zm16 32a9 9 0 0 0-1 18c5 0 10-3 10-8v-1c0-5-4-9-9-9Zm31 0a9 9 0 0 0-9 9c0 6 4 9 9 9 6 0 9-4 9-9s-4-9-9-9Zm31 0a9 9 0 0 0 0 18c5 1 9-4 10-9 0-5-5-9-10-9Zm44 1c-6-1-9-2-10-8l-2-10-1-20c-1-16-12-21-28-19v14h6c4 0 6 1 7 5v11l3 21c1 6 5 11 10 14-9 6-11 14-12 24l-1 19c0 6-2 8-8 8h-5v14h10c10 0 16-5 18-15l1-16 1-15c1-7 4-10 12-11h2v-16h-3Z" fill="#173647"/></svg>

After

Width:  |  Height:  |  Size: 836 B

+32 -32
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -247,7 +247,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -278,7 +278,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -294,7 +294,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -310,7 +310,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -335,7 +335,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -354,7 +354,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -364,7 +364,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -570,7 +570,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -637,7 +637,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -710,7 +710,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -769,13 +769,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -786,7 +786,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -803,7 +803,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -811,13 +811,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -914,7 +914,7 @@ This method saves data in a way that makes it available to [exception log adapte
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -938,7 +938,7 @@ Behaves like `noData()` except that it sets the status code to 204 and the Conte
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -956,7 +956,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -965,7 +965,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -983,7 +983,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -1031,7 +1031,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -1044,7 +1044,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -1069,7 +1069,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -1082,7 +1082,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1098,7 +1098,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+20 -20
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -101,7 +101,7 @@ Implementing OAuth is something I would like to document, but it is a fairly lar
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -148,7 +148,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -179,7 +179,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -199,7 +199,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -218,7 +218,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -228,7 +228,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -453,7 +453,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -488,13 +488,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js ```cfscript
return newRepresentation().noData().withStatus(403); return newRepresentation().noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return newRepresentation() return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."}) .setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403); .withStatus(403);
@@ -504,7 +504,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) { function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue"; arguments.requestArguments.myData = "myvalue";
return true; return true;
@@ -513,7 +513,7 @@ function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData = myValue //arguments.myData = myValue
} }
@@ -530,7 +530,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -543,7 +543,7 @@ return noData().withStatus(404);
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._ This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js ```cfscript
return representationOf(queryToArray(someQuery)); return representationOf(queryToArray(someQuery));
``` ```
@@ -603,7 +603,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -616,7 +616,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -632,7 +632,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+20 -20
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -101,7 +101,7 @@ Implementing OAuth is something I would like to document, but it is a fairly lar
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -148,7 +148,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -179,7 +179,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -199,7 +199,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -218,7 +218,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -228,7 +228,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -453,7 +453,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -488,13 +488,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js ```cfscript
return newRepresentation().noData().withStatus(403); return newRepresentation().noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return newRepresentation() return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."}) .setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403); .withStatus(403);
@@ -504,7 +504,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) { function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue"; arguments.requestArguments.myData = "myvalue";
return true; return true;
@@ -513,7 +513,7 @@ function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData = myValue //arguments.myData = myValue
} }
@@ -530,7 +530,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -543,7 +543,7 @@ return noData().withStatus(404);
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._ This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js ```cfscript
return representationOf(queryToArray(someQuery)); return representationOf(queryToArray(someQuery));
``` ```
@@ -603,7 +603,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -616,7 +616,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -632,7 +632,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+23 -23
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -128,7 +128,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -196,7 +196,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -227,7 +227,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -247,7 +247,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -266,7 +266,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -276,7 +276,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -501,7 +501,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -537,13 +537,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js ```cfscript
return newRepresentation().noData().withStatus(403); return newRepresentation().noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return newRepresentation() return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."}) .setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403); .withStatus(403);
@@ -553,7 +553,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) { function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue"; arguments.requestArguments.myData = "myvalue";
return true; return true;
@@ -562,7 +562,7 @@ function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -570,13 +570,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -606,7 +606,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -619,7 +619,7 @@ return noData().withStatus(404);
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._ This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js ```cfscript
return representationOf(queryToArray(someQuery)); return representationOf(queryToArray(someQuery));
``` ```
@@ -661,7 +661,7 @@ Use this method in place of `representationOf()` to return a stream of binary da
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js ```cfscript
return streamFile("/foo.txt").andDelete(true); return streamFile("/foo.txt").andDelete(true);
``` ```
@@ -683,7 +683,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -696,7 +696,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -712,7 +712,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+23 -23
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -200,7 +200,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -231,7 +231,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -251,7 +251,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -270,7 +270,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -280,7 +280,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -505,7 +505,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -541,13 +541,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js ```cfscript
return newRepresentation().noData().withStatus(403); return newRepresentation().noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return newRepresentation() return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."}) .setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403); .withStatus(403);
@@ -557,7 +557,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) { function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue"; arguments.requestArguments.myData = "myvalue";
return true; return true;
@@ -566,7 +566,7 @@ function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -574,13 +574,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -610,7 +610,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -623,7 +623,7 @@ return noData().withStatus(404);
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._ This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js ```cfscript
return representationOf(queryToArray(someQuery)); return representationOf(queryToArray(someQuery));
``` ```
@@ -665,7 +665,7 @@ Use this method in place of `representationOf()` to return a stream of binary da
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js ```cfscript
return streamFile("/foo.txt").andDelete(true); return streamFile("/foo.txt").andDelete(true);
``` ```
@@ -687,7 +687,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -700,7 +700,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -716,7 +716,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+22 -22
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -200,7 +200,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -231,7 +231,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -251,7 +251,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -270,7 +270,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -280,7 +280,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -500,7 +500,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -536,13 +536,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js ```cfscript
return newRepresentation().noData().withStatus(403); return newRepresentation().noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return newRepresentation() return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."}) .setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403); .withStatus(403);
@@ -552,7 +552,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) { function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue"; arguments.requestArguments.myData = "myvalue";
return true; return true;
@@ -561,7 +561,7 @@ function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -569,13 +569,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -605,7 +605,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -665,7 +665,7 @@ Use this method in place of `representationOf()` to return a stream of binary da
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js ```cfscript
return streamFile("/foo.txt").andDelete(true); return streamFile("/foo.txt").andDelete(true);
``` ```
@@ -687,7 +687,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -700,7 +700,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -716,7 +716,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+22 -22
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -200,7 +200,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -231,7 +231,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -251,7 +251,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -270,7 +270,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -280,7 +280,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -500,7 +500,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -542,13 +542,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js ```cfscript
return newRepresentation().noData().withStatus(403); return newRepresentation().noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return newRepresentation() return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."}) .setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403); .withStatus(403);
@@ -558,7 +558,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) { function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue"; arguments.requestArguments.myData = "myvalue";
return true; return true;
@@ -567,7 +567,7 @@ function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -575,13 +575,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -611,7 +611,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -671,7 +671,7 @@ Use this method in place of `representationOf()` to return a stream of binary da
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js ```cfscript
return streamFile("/foo.txt").andDelete(true); return streamFile("/foo.txt").andDelete(true);
``` ```
@@ -693,7 +693,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -706,7 +706,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -722,7 +722,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+27 -27
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Railo
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -217,7 +217,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -252,7 +252,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -272,7 +272,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -291,7 +291,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -301,7 +301,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -508,7 +508,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -570,7 +570,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -621,13 +621,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -638,7 +638,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -655,7 +655,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -663,13 +663,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -736,7 +736,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -752,7 +752,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToArray(someQ, function (row) { return queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -773,7 +773,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -821,7 +821,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -834,7 +834,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -859,7 +859,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -872,7 +872,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -888,7 +888,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
@@ -1088,7 +1088,7 @@ A new argument has been appended to onTaffyRequest, where Taffy will pass the or
This makes the latest onTaffyRequest method signature: This makes the latest onTaffyRequest method signature:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
+30 -30
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -252,7 +252,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -272,7 +272,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -291,7 +291,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -301,7 +301,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -462,7 +462,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -522,7 +522,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -584,7 +584,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -635,13 +635,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -652,7 +652,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -669,7 +669,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -677,13 +677,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -768,7 +768,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -784,7 +784,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -793,7 +793,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -811,7 +811,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -859,7 +859,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -872,7 +872,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -897,7 +897,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -910,7 +910,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -926,7 +926,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
@@ -1116,7 +1116,7 @@ And thanks to [a pull request](https://github.com/atuttle/pull/256) from @dskagg
Previously, the LogToEmail adapter supported only **emailFrom, emailTo, emailSubj,** and **emailType** config attributes, which were used to send the email. Now all cfmail attributes are supported (and properly named: from, to, subject, type, server, username, password, etc &mdash; and still backwards compatible with emailSubj, emailTo, emailFrom, and emailType). Here's an example [framework.exceptionLogAdapterConfig](#exceptionlogadapterconfig): Previously, the LogToEmail adapter supported only **emailFrom, emailTo, emailSubj,** and **emailType** config attributes, which were used to send the email. Now all cfmail attributes are supported (and properly named: from, to, subject, type, server, username, password, etc &mdash; and still backwards compatible with emailSubj, emailTo, emailFrom, and emailType). Here's an example [framework.exceptionLogAdapterConfig](#exceptionlogadapterconfig):
```js ```cfscript
variables.framework.exceptionLogAdapter = "taffy.bonus.LogToEmail"; variables.framework.exceptionLogAdapter = "taffy.bonus.LogToEmail";
variables.framework.exceptionLogAdapterConfig = { variables.framework.exceptionLogAdapterConfig = {
to: "errors@example.com", to: "errors@example.com",
@@ -1166,7 +1166,7 @@ GET /translations?language[]=en&language[]=fr&language[]=de
jQuery in particular (among others), will send data in this format, given the following input: jQuery in particular (among others), will send data in this format, given the following input:
```js ```cfscript
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: "translations", url: "translations",
+32 -32
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -251,7 +251,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -282,7 +282,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -298,7 +298,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -314,7 +314,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -339,7 +339,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -358,7 +358,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -368,7 +368,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -529,7 +529,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -589,7 +589,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -651,7 +651,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -702,13 +702,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -719,7 +719,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -736,7 +736,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -744,13 +744,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -837,7 +837,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -853,7 +853,7 @@ return rep(
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -869,7 +869,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -878,7 +878,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -896,7 +896,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -944,7 +944,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -957,7 +957,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -982,7 +982,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -995,7 +995,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1011,7 +1011,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+32 -32
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -247,7 +247,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -278,7 +278,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -294,7 +294,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -310,7 +310,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -335,7 +335,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -354,7 +354,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -364,7 +364,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -570,7 +570,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -637,7 +637,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -710,7 +710,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -769,13 +769,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -786,7 +786,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -803,7 +803,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -811,13 +811,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -905,7 +905,7 @@ Resource CFCs extend `taffy.core.resource`. The following methods are available
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -929,7 +929,7 @@ Behaves like `noData()` except that it sets the status code to 204 and the Conte
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -947,7 +947,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -956,7 +956,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -974,7 +974,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -1022,7 +1022,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -1035,7 +1035,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -1060,7 +1060,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -1073,7 +1073,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1089,7 +1089,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+32 -32
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -247,7 +247,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -278,7 +278,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -294,7 +294,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -310,7 +310,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -335,7 +335,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -354,7 +354,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -364,7 +364,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -570,7 +570,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -637,7 +637,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -710,7 +710,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -769,13 +769,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -786,7 +786,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -803,7 +803,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -811,13 +811,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -914,7 +914,7 @@ This method saves data in a way that makes it available to [exception log adapte
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -938,7 +938,7 @@ Behaves like `noData()` except that it sets the status code to 204 and the Conte
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -956,7 +956,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -965,7 +965,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -983,7 +983,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -1031,7 +1031,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -1044,7 +1044,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -1069,7 +1069,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -1082,7 +1082,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1098,7 +1098,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+32 -32
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -247,7 +247,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -278,7 +278,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -294,7 +294,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -310,7 +310,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -335,7 +335,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -354,7 +354,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -364,7 +364,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -570,7 +570,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -637,7 +637,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -710,7 +710,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -769,13 +769,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -786,7 +786,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -803,7 +803,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -811,13 +811,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -914,7 +914,7 @@ This method saves data in a way that makes it available to [exception log adapte
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -938,7 +938,7 @@ Behaves like `noData()` except that it sets the status code to 204 and the Conte
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -956,7 +956,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -965,7 +965,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -983,7 +983,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -1031,7 +1031,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -1044,7 +1044,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -1069,7 +1069,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -1082,7 +1082,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1098,7 +1098,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+32 -32
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -77,13 +77,13 @@ You can see that the taffy folder is a sibling to Application.cfc. This allows A
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js ```cfscript
this.mappings["/resources"] = expandPath("./resources"); this.mappings["/resources"] = expandPath("./resources");
``` ```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +132,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -221,7 +221,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -247,7 +247,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -278,7 +278,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -294,7 +294,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -310,7 +310,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -335,7 +335,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -354,7 +354,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -364,7 +364,7 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
@@ -570,7 +570,7 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
@@ -637,7 +637,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -710,7 +710,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -769,13 +769,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -786,7 +786,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -803,7 +803,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -811,13 +811,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -914,7 +914,7 @@ This method saves data in a way that makes it available to [exception log adapte
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -938,7 +938,7 @@ Behaves like `noData()` except that it sets the status code to 204 and the Conte
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -956,7 +956,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -965,7 +965,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -983,7 +983,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -1031,7 +1031,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -1044,7 +1044,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -1069,7 +1069,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -1082,7 +1082,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1098,7 +1098,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+178 -41
View File
@@ -12,7 +12,7 @@ The REST Web Service framework for ColdFusion and Lucee
**Application.cfc:** **Application.cfc:**
```js ```cfscript
component extends="taffy.core.api" {} component extends="taffy.core.api" {}
``` ```
@@ -24,7 +24,7 @@ component extends="taffy.core.api" {}
**/resources/hello.cfc:** **/resources/hello.cfc:**
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" { component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){ function get(){
@@ -75,15 +75,19 @@ Using sub-folders requires the use of Application-Specific Mappings (introduced
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`. You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes. Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to do one of the following:
```js - Add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
this.mappings["/resources"] = expandPath("./resources");
``` ```cfscript
this.mappings["/resources"] = expandPath("../api/resources");
```
- You can specify the path to your resource components using the [`resourcesCFCPath`](#resourcescfcpath) setting to specify the dotted path to your resource folder. This will allow you to store your resource components in the directory of your choosing.
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder): You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js ```cfscript
this.mappings["/taffy"] = expandPath("./taffy"); this.mappings["/taffy"] = expandPath("./taffy");
``` ```
@@ -132,7 +136,7 @@ In the xml above you can see that I only have 1 wildcard, but to compensate I've
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as: Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js ```cfscript
return representationOf({...}).withStatus(403, "Not Authorized"); return representationOf({...}).withStatus(403, "Not Authorized");
``` ```
@@ -173,6 +177,54 @@ _Thanks to Brook Davies for providing the solution!_
Not all HTTP clients will allow you to easily send PUT or DELETE requests (sometimes not at all). The standard method for circumventing this restriction, which Taffy supports, is by sending your request as a POST with the header `X-HTTP-METHOD-OVERRIDE` and setting its value to PUT/DELETE as needed. Taffy will detect this header and treat the request as if it were a PUT/DELETE request. Not all HTTP clients will allow you to easily send PUT or DELETE requests (sometimes not at all). The standard method for circumventing this restriction, which Taffy supports, is by sending your request as a POST with the header `X-HTTP-METHOD-OVERRIDE` and setting its value to PUT/DELETE as needed. Taffy will detect this header and treat the request as if it were a PUT/DELETE request.
## OpenAPI / Swagger
**Available in:** Taffy 4.0+
Taffy generates an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) document describing your API automatically, using the same resource/argument metadata that powers the dashboard and docs. No extra annotations required — just hit:
- `index.cfm?openapi` — returns the spec as `application/json`
- `index.cfm?swagger` — alias for the same endpoint
The generated document can be dropped into [Swagger UI](https://swagger.io/tools/swagger-ui/), [Stoplight Studio](https://stoplight.io/studio), Postman, [swagger.io Editor](https://editor.swagger.io/), or any other OpenAPI-aware tool.
### What gets generated
| OpenAPI field | Source |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `info.title` | `variables.framework.docs.APIName` |
| `info.version` | `variables.framework.docs.APIVersion` |
| `info.description` / `contact` / `license` | `variables.framework.openapi` (optional) |
| `servers[0].url` | Auto-derived from `cgi.http_host` + script path (override via `openapi.servers`) |
| `paths` | Each resource's `taffy:uri` |
| `operationId` | `{beanName}_{verb}` |
| `summary` / `tags` | `taffy:docs:name` if set, else the bean name |
| `description` | Function `hint` |
| `parameters` (path) | URI tokens — always emitted, even if the resource doesn't declare them as args |
| `parameters` (query) | Non-token args on `GET`/`DELETE`/`HEAD`/`OPTIONS` methods |
| `requestBody` | Non-token args on `POST`/`PUT`/`PATCH` — emitted under both `application/json` and `application/x-www-form-urlencoded` |
| Parameter `schema.type` / `format` | Mapped from the CFML arg `type` (`numeric`, `boolean`, `date`, `uuid`, etc.) |
| Parameter `description` | Argument `hint` |
| Parameter `default` | Argument `default` (when non-empty) |
| Response content types | MIME types from all registered serializers (via `taffy:mime`) |
### Hiding things from the spec
The same metadata flags that hide resources from the dashboard also hide them from the OpenAPI spec:
- `taffy:docs:hide` / `taffy_docs_hide` — component, function, or argument
- `taffy:dashboard:hide` / `taffy_dashboard_hide` — component, function, or argument
Custom `taffy:verb` values (anything outside the standard `get`/`put`/`post`/`delete`/`options`/`head`/`patch`/`trace`) are silently dropped — OpenAPI doesn't accept arbitrary verb names.
### Performance
The spec is generated once on the first request to `?openapi` (or `?swagger`) and cached in `application._taffy`. Subsequent requests serve the cached JSON string directly — no CFC instantiation, no metadata walking, no serialization. The cache is invalidated automatically on framework reload.
### Configuration
See [openapi](#openapi) below for the full config reference.
## More Guides ## More Guides
Some guides are too broad for this document. For your benefit, they are linked here: Some guides are too broad for this document. For your benefit, they are linked here:
@@ -221,7 +273,7 @@ If you want a resource not to be included in the dashboard, set `taffy:dashboard
or or
```js ```cfscript
component taffy_uri="/secret-squirrel" taffy_dashboard_hide { component taffy_uri="/secret-squirrel" taffy_dashboard_hide {
} }
``` ```
@@ -247,7 +299,7 @@ The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{
or or
```js ```cfscript
component taffy_uri="/artist/{artistId}" { component taffy_uri="/artist/{artistId}" {
} }
``` ```
@@ -278,7 +330,7 @@ By convention, resources will automatically map the 4 primary HTTP REST verbs --
or or
```js ```cfscript
function getUser( numeric userId ) taffy_verb="get" { function getUser( numeric userId ) taffy_verb="get" {
} }
``` ```
@@ -294,7 +346,7 @@ You can prevent a resource from showing in the documentation by adding the `taff
or or
```js ```cfscript
component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide { component extends="taffy.core.resource" taffy_uri="/artist/{artistId}" taffy_docs_hide {
} }
``` ```
@@ -310,7 +362,7 @@ Similarly, you can hide methods and parameters by adding the attribute to the `<
or or
```js ```cfscript
public function getUser( public function getUser(
required numeric userId, required numeric userId,
string _hidden = "" taffy_docs_hide string _hidden = "" taffy_docs_hide
@@ -320,6 +372,27 @@ taffy_docs_hide
} }
``` ```
##### Argument Constraint Metadata
You can document value constraints on function arguments using the following metadata attributes. These are **documentation-only** — Taffy does not enforce them at runtime, but they are displayed on the dashboard and generated docs as inline badges next to the argument name.
| Attribute | Applies to | Description |
| ----------------- | ---------- | ---------------------------------- |
| `taffy_minlength` | string | Minimum string length |
| `taffy_maxlength` | string | Maximum string length |
| `taffy_min` | numeric | Minimum numeric value |
| `taffy_max` | numeric | Maximum numeric value |
| `taffy_pattern` | string | Regex pattern the value must match |
```cfscript
function get(
required string changedOnDate = "" taffy_minlength="10" taffy_maxlength="10" taffy_pattern="^\d{4}-\d{2}-\d{2}$",
numeric page = 1 taffy_min="1",
numeric pageSize = 25 taffy_min="1" taffy_max="100"
) {
}
```
#### In Serializers #### In Serializers
Serializers are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Serializer is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited. Serializers are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Serializer is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
@@ -335,7 +408,7 @@ By convention, the mime-types supported by your API are determined by the method
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" { function getAsJson() taffy_mime="application/json" {
} }
``` ```
@@ -354,7 +427,7 @@ When your API supports more than one data format (i.e. json and xml), you must s
or or
```js ```cfscript
function getAsJson() taffy_mime="application/json" taffy_default="true" {} function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {} function getAsXml() taffy_mime="application/xml" {}
@@ -364,8 +437,9 @@ function getAsXml() taffy_mime="application/xml" {}
Default values: Default values:
```js ```cfscript
variables.framework = { variables.framework = {
resourcesCFCPath = "",
reloadKey = "reload", reloadKey = "reload",
reloadPassword = "true", reloadPassword = "true",
reloadOnEveryRequest = false, reloadOnEveryRequest = false,
@@ -382,6 +456,7 @@ variables.framework = {
disabledDashboardRedirect = "", disabledDashboardRedirect = "",
dashboardHeaders = {}, dashboardHeaders = {},
showDocsWhenDashboardDisabled = false, showDocsWhenDashboardDisabled = false,
allowGoogleFonts = true,
docs = { docs = {
APIName = "", APIName = "",
APIVersion = "" APIVersion = ""
@@ -396,21 +471,33 @@ variables.framework = {
unhandledPaths = "/flex2gateway", unhandledPaths = "/flex2gateway",
allowCrossDomain = false, allowCrossDomain = false,
exposeTaffyHeaders = true,
globalHeaders = structNew(), globalHeaders = structNew(),
debugKey = "debug", debugKey = "debug",
useEtags = false, useEtags = false,
returnExceptionsAsJson = true, returnExceptionsAsJson = false,
exceptionLogAdapter = "taffy.bonus.LogToScreen", exceptionLogAdapter = "taffy.bonus.LogToScreen",
exceptionLogAdapterConfig = {}, exceptionLogAdapterConfig = {},
beanFactory = "", beanFactory = "",
openapi = {
enabled = true
},
environments = {} environments = {}
}; };
``` ```
#### resourcesCFCPath
**Available in:** Taffy 3.8+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** By default, Taffy will attempt to load your resource components from either a child folder named `resources` or from a CF mapping named `resources`. You can use this setting to define an explicit path to your resource components using the "dotted" path of your resource folder (e.g. `myapp.api.rest-cfcs`).
#### reloadKey #### reloadKey
**Available in:** Taffy 1.2+<br/> **Available in:** Taffy 1.2+<br/>
@@ -497,6 +584,13 @@ To provide a simulated response, add an additional method to your Resource CFCs
**Default:** False<br/> **Default:** False<br/>
**Description:** Whether or not Taffy will display user friendly documentation when the dashboard is disabled. **Description:** Whether or not Taffy will display user friendly documentation when the dashboard is disabled.
#### allowGoogleFonts
**Available in:** Taffy 4.0+<br/>
**Type:** Boolean<br/>
**Default:** True<br/>
**Description:** When true, the dashboard and documentation pages load the [Atkinson Hyperlegible](https://fonts.google.com/specimen/Atkinson+Hyperlegible) and [Atkinson Hyperlegible Mono](https://fonts.google.com/specimen/Atkinson+Hyperlegible+Mono) fonts from Google Fonts. Set to false to prevent any external requests to Google, in which case the dashboard falls back to system fonts.
#### docs.APIName #### docs.APIName
**Available in:** Taffy 3.0+<br/> **Available in:** Taffy 3.0+<br/>
@@ -570,11 +664,32 @@ The allowed verbs, of course, are the ones allowed by the requested resource, as
In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list: In addition, as of Taffy 3.1.0, you can set this setting to a string of allowable hosts, as a (comma, semicolon, or space) delimited list:
```js ```cfscript
variables.framework.allowCrossDomain = variables.framework.allowCrossDomain =
"http://example.com; http://foo.bar, http://google.com"; "http://example.com; http://foo.bar, http://google.com";
``` ```
#### exposeTaffyHeaders
**Available in:** Taffy 3.8+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** Determines if the standard Taffy debug HTTP response headers should be included with each request. The Taffy debug headers are:
- `X-TAFFY-RELOADED` — Determines if the Taffy configuration was reloaded on the request.
- `X-TIME-TO-RELOAD` — The time it took for Taffy to initialize.
- `X-TIME-IN-PARSE` — The time it took to parse the request.
- `X-TIME-IN-ONTAFFYREQUEST` — The time spent in the `onTaffyRequest` method.
- `X-TIME-IN-RESOURCE` — The time spent executing the requested resource.
- `X-TIME-IN-CACHE-CHECK` — The time spent checking for a cached response.
- `X-TIME-IN-CACHE-GET` — The time spent to retrieve a cached response.
- `X-TIME-IN-CACHE-SAVE` — The time spent to save a cached response.
- `X-TIME-IN-SERIALIZE` — The time spent serializing the response.
- `X-TIME-IN-TAFFY` — The time spent executing the Taffy internals.
- `X-TIME-IN-ONTAFFYREQUESTEND` — The time spent in the `onTaffyRequestEnd` method.
Setting this to `false` will prevent these response headers from being written to the HTTP stream.
#### globalHeaders #### globalHeaders
**Available in:** Taffy 1.2+<br/> **Available in:** Taffy 1.2+<br/>
@@ -611,15 +726,15 @@ Global headers are static. You set them on application initialization and they d
**Available in:** Taffy 1.2+<br/> **Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/> **Type:** Boolean<br/>
**Default:** true<br/> **Default:** false<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format). As of Taffy 2.1 this also includes a structured stack trace with file names and line numbers. **Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format). As of Taffy 2.1 this also includes a structured stack trace with file names and line numbers. Default changed to `false` in Taffy 4.x to prevent accidental stack trace disclosure in production.
#### exceptionLogAdapter #### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/> **Available in:** Taffy 1.2+<br/>
**Type:** String<br/> **Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/> **Default:** "taffy.bonus.LogToDevNull"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details. **Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter discards all exceptions silently. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### exceptionLogAdapterConfig #### exceptionLogAdapterConfig
@@ -637,7 +752,7 @@ Global headers are static. You set them on application initialization and they d
**NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example: **NOTE FOR EXTERNAL BEAN FACTORY USERS (e.g. Coldspring, DI/1 etc)** If your external bean factory is initialized in onApplicationStart then you need to set the variables.framework.beanFactory after your bean factory has initialized, for example:
```js ```cfscript
component extends="taffy.core.api" component extends="taffy.core.api"
{ {
this.name = 'myapi'; this.name = 'myapi';
@@ -652,6 +767,28 @@ component extends="taffy.core.api"
} }
``` ```
#### openapi
**Available in:** Taffy 4.0+<br/>
**Type:** Structure<br/>
**Default:** `{ enabled: true }`<br/>
**Description:** Controls the OpenAPI 3.1 spec endpoint served at `?openapi` / `?swagger`. Set `enabled` to `false` to disable the endpoint entirely (returns 403). Optional keys `description`, `contact`, and `license` are passed through to the spec's `info` block. `servers` (array of `{ url, description }` objects) overrides the auto-derived server URL.
```cfscript
variables.framework.openapi = {
enabled = true,
description = "Public API for the Foo platform.",
contact = { name = "API Team", email = "api@example.com" },
license = { name = "MIT", url = "https://opensource.org/licenses/MIT" },
servers = [
{ url = "https://api.example.com", description = "Production" },
{ url = "https://staging.api.example.com", description = "Staging" }
]
};
```
See the [OpenAPI / Swagger](#openapi--swagger) section for details on what gets generated.
#### environments #### environments
**Available in:** Taffy 1.3+<br/> **Available in:** Taffy 1.3+<br/>
@@ -710,7 +847,7 @@ Taffy calls this method during initialization to determine in which configured e
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production: The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js ```cfscript
variables.framework = { variables.framework = {
disableDashboard = false disableDashboard = false
@@ -769,13 +906,13 @@ This method is optional, and allows you to inspect and potentially abort an API
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this: If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to call and return the result of either **[noData()](#nodata)** or **[representationOf()](#representationof)**. If you simply want to return with a status code of 403 (which indicates "Not Allowed") and no response body, you could do this:
```js ```cfscript
return noData().withStatus(403); return noData().withStatus(403);
``` ```
Alternately, you could return some data to indicate that they owe you money or something: Alternately, you could return some data to indicate that they owe you money or something:
```js ```cfscript
return representationOf({ return representationOf({
error="Your account is past due. Please email accounts payable." error="Your account is past due. Please email accounts payable."
}) })
@@ -786,7 +923,7 @@ The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure: You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js ```cfscript
function onTaffyRequest( function onTaffyRequest(
verb, verb,
cfc, cfc,
@@ -803,7 +940,7 @@ function onTaffyRequest(
In your resource: In your resource:
```js ```cfscript
function get(myData) { function get(myData) {
//arguments.myData => "myValue" //arguments.myData => "myValue"
} }
@@ -811,13 +948,13 @@ function get(myData) {
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method: You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js ```cfscript
public function getData( id ) taffy_method="get" role="datareader" { ... } public function getData( id ) taffy_method="get" role="datareader" { ... }
``` ```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc: Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js ```cfscript
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){ function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key... local.user = (...); //get user from api key...
@@ -914,7 +1051,7 @@ This method saves data in a way that makes it available to [exception log adapte
This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers. This is a convenience method used to make sure that certain all-numeric inputs get serialized as a string in the output not converted to numeric output. Examples are postal codes or phone numbers.
```js ```cfscript
return rep( return rep(
queryToArray(myQuery, function (row) { queryToArray(myQuery, function (row) {
row.phone = encode.string(row.phone); row.phone = encode.string(row.phone);
@@ -938,7 +1075,7 @@ Behaves like `noData()` except that it sets the status code to 204 and the Conte
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so: This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js ```cfscript
return noData().withStatus(404); return noData().withStatus(404);
``` ```
@@ -956,7 +1093,7 @@ This method transforms a ColdFusion query object into an array of structures. It
The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function can be used to efficiently transform keys in the structure before it is added to the array without additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
queryToArray(someQ, function (row) { queryToArray(someQ, function (row) {
row.startDate = dateFormat(row.startDate, "yyyy-mm-dd"); row.startDate = dateFormat(row.startDate, "yyyy-mm-dd");
return row; return row;
@@ -965,7 +1102,7 @@ queryToArray(someQ, function (row) {
When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call: When you want to return the resulting array as your API response, you must still wrap it in a [representationOf](#rep-1) call:
```js ```cfscript
return rep(queryToArray(someQ)); return rep(queryToArray(someQ));
``` ```
@@ -983,7 +1120,7 @@ This method transforms a ColdFusion query object into a structure. If there is m
The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.** The callback function is passed the column name and the value, and can be used to efficiently transform keys in the structure as they are read from the query without need for additional looping. For example, you can use it to format dates in a particular style. **Your callback must return a value.**
```js ```cfscript
return queryToStruct(someQ, function (colName, val) { return queryToStruct(someQ, function (colName, val) {
if (colName == "startDate") { if (colName == "startDate") {
return dateFormat(val, "yyyy-mm-dd"); return dateFormat(val, "yyyy-mm-dd");
@@ -1020,7 +1157,7 @@ Data can be of any type, including complex data types like queries, structures,
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, LogToLog, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface. What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, LogToLog, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters. If you don't configure a logging adapter, the default is LogToDevNull, which silently discards exceptions. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary() #### streamBinary()
@@ -1031,7 +1168,7 @@ If you don't configure a logging adapter, the default is LogToEmail, but the def
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamBinary(local.pdf).withStatus(200).withMime("application/pdf"); return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
``` ```
@@ -1044,7 +1181,7 @@ return streamBinary(local.pdf).withStatus(200).withMime("application/pdf");
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example: Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete. **Note: ** When streaming binary data as the response, you must set the mime type manually using [withMime()](#withmime). For example:
```js ```cfscript
return streamFile("/foo.txt") return streamFile("/foo.txt")
.andDelete(true) .andDelete(true)
.withStatus(200) .withStatus(200)
@@ -1069,7 +1206,7 @@ Use this method in place of `representationOf()` to stream an image from disk (o
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional. This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js ```cfscript
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"}); return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
``` ```
@@ -1082,7 +1219,7 @@ return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return. This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js ```cfscript
return streamFile("kittens/cuteness.pdf").withMime("application/pdf"); return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
``` ```
@@ -1098,7 +1235,7 @@ This special method _**requires**_ the use of either **noData** or **representat
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._ _If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js ```cfscript
return noData().withStatus(404, "Not Found"); return noData().withStatus(404, "Not Found");
``` ```
+12
View File
@@ -0,0 +1,12 @@
<!-- docs/_coverpage.md -->
# Taffy <small>v4</small>
> The REST web service framework for ColdFusion &amp; Lucee.
A full REST API in a single file. Opinionated defaults. Zero boilerplate.
Content negotiation, CORS, an interactive dashboard, and OpenAPI JSON — included.
[Get Started](#quickstart-your-first-api)
[Download v4.0.0](https://github.com/atuttle/Taffy/archive/v4.0.0.zip)
[GitHub &#8599;](https://github.com/atuttle/Taffy)
+924 -25
View File
@@ -2,46 +2,945 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Document</title> <title>Taffy — REST Framework for ColdFusion &amp; Lucee</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta <meta
name="description" name="description"
content="Documentation for Taffy — The REST Web Service Framework for ColdFusion and Lucee" content="Taffy is a REST web service framework for ColdFusion and Lucee. Write an API in a single file. Docs for every version."
/>
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/> />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=JetBrains+Mono:wght@400;600&display=swap"
rel="stylesheet" rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify/lib/themes/buble.css"
/> />
<style>
/* ---------- Theme tokens ---------- */
:root {
--font-sans: "Atkinson Hyperlegible", ui-sans-serif, system-ui,
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* Light palette (default) */
--bg: #fdf8f2;
--bg-elev: #ffffff;
--bg-sidebar: #f6ede0;
--bg-code: #f2e7d5;
--bg-inline-code: #f2e4cf;
--text: #1c1a18;
--text-muted: #6b5f52;
--text-subtle: #8b7f72;
--border: #e7d9c2;
--border-strong: #d9c6a8;
--accent: #c8396e; /* taffy pink */
--accent-hover: #a82557;
--accent-contrast: #ffffff;
--accent-soft: rgba(200, 57, 110, 0.12);
--warn: #b4651f;
--shadow: 0 1px 2px rgba(28, 26, 24, 0.04),
0 4px 12px rgba(28, 26, 24, 0.06);
--shadow-lg: 0 10px 30px rgba(28, 26, 24, 0.12);
--radius: 10px;
--radius-sm: 6px;
--radius-lg: 16px;
--content-width: 820px;
--sidebar-width: 280px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #161310;
--bg-elev: #1f1a16;
--bg-sidebar: #1a1613;
--bg-code: #24201c;
--bg-inline-code: #2a241e;
--text: #f2e9dd;
--text-muted: #b3a796;
--text-subtle: #8a7e6f;
--border: #332c24;
--border-strong: #483d31;
--accent: #ff7aa8;
--accent-hover: #ff9ec0;
--accent-contrast: #1a0d14;
--accent-soft: rgba(255, 122, 168, 0.14);
--warn: #e3a36a;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4),
0 4px 12px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5);
}
img.logo-invert-on-dark {
filter: brightness(0.92);
}
}
/* ---------- Base ---------- */
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: 17px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
min-height: 100dvh;
}
a {
color: var(--accent);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 0.15s ease, border-color 0.15s ease;
}
a:hover {
color: var(--accent-hover);
border-bottom-color: var(--accent-hover);
}
::selection {
background: var(--accent-soft);
color: var(--text);
}
/* ---------- Sidebar ---------- */
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: var(--sidebar-width);
padding: 28px 22px 28px 28px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
overflow-y: auto;
overflow-x: hidden;
z-index: 10;
transition: transform 0.25s ease;
}
.sidebar::-webkit-scrollbar {
width: 8px;
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--border-strong);
border-radius: 8px;
}
.sidebar .app-name {
margin-bottom: 18px;
padding-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.sidebar .app-name-link {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 700;
color: var(--text);
border-bottom: 0;
letter-spacing: -0.01em;
}
.sidebar .app-name-link::before {
content: "";
width: 26px;
height: 26px;
border-radius: 50%;
background: radial-gradient(
circle at 30% 30%,
#ff9abf,
var(--accent) 70%
);
box-shadow: inset 0 -3px 6px rgba(0, 0, 0, 0.15);
flex-shrink: 0;
}
.sidebar .search {
margin: 0 0 14px 0;
padding: 0 0 14px 0;
border-bottom: 1px solid var(--border);
}
.sidebar .search input {
width: 100%;
padding: 8px 10px;
font-family: var(--font-sans);
font-size: 14px;
color: var(--text);
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
outline: none;
}
.sidebar .search input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.sidebar-nav ul {
list-style: none;
margin: 0;
padding-left: 0;
}
.sidebar-nav > ul > li {
margin: 2px 0;
}
.sidebar-nav ul ul {
padding-left: 12px;
border-left: 1px solid var(--border);
margin-left: 4px;
}
.sidebar-nav a {
display: block;
padding: 5px 10px;
color: var(--text-muted);
font-size: 14.5px;
border-radius: var(--radius-sm);
border-bottom: 0;
line-height: 1.4;
}
.sidebar-nav a:hover {
color: var(--text);
background: var(--accent-soft);
}
.sidebar-nav a.active,
.sidebar-nav li.active > a {
color: var(--accent);
font-weight: 700;
background: var(--accent-soft);
}
.sidebar-nav p {
margin: 16px 0 4px;
padding: 0 10px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-subtle);
font-weight: 700;
}
/* ---------- Sidebar toggle (mobile) ---------- */
.sidebar-toggle {
position: fixed;
top: 16px;
left: 16px;
z-index: 30;
width: 40px;
height: 40px;
display: none;
align-items: center;
justify-content: center;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
box-shadow: var(--shadow);
}
.sidebar-toggle .sidebar-toggle-button {
width: 18px;
height: 14px;
position: relative;
}
.sidebar-toggle span {
position: absolute;
left: 0;
right: 0;
height: 2px;
background: var(--text);
border-radius: 2px;
}
.sidebar-toggle span:nth-child(1) {
top: 0;
}
.sidebar-toggle span:nth-child(2) {
top: 6px;
}
.sidebar-toggle span:nth-child(3) {
top: 12px;
}
/* ---------- Content ---------- */
main {
display: block;
min-height: 100dvh;
}
.content {
margin-left: var(--sidebar-width);
padding: 0;
}
.markdown-section {
max-width: var(--content-width);
margin: 0 auto;
padding: 64px 40px 120px;
}
.markdown-section > :first-child {
margin-top: 0;
}
/* Typography */
.markdown-section h1,
.markdown-section h2,
.markdown-section h3,
.markdown-section h4,
.markdown-section h5,
.markdown-section h6 {
font-weight: 700;
letter-spacing: -0.015em;
line-height: 1.25;
color: var(--text);
}
.markdown-section h1 {
font-size: 2.4rem;
margin: 0 0 0.4em;
letter-spacing: -0.025em;
}
.markdown-section h2 {
font-size: 1.75rem;
margin: 2.2em 0 0.6em;
padding-bottom: 0.35em;
border-bottom: 1px solid var(--border);
}
.markdown-section h3 {
font-size: 1.3rem;
margin: 1.8em 0 0.5em;
}
.markdown-section h4 {
font-size: 1.1rem;
margin: 1.5em 0 0.4em;
}
.markdown-section p {
margin: 0.9em 0;
}
.markdown-section strong {
font-weight: 700;
}
.markdown-section blockquote {
margin: 1.2em 0;
padding: 0.1em 1.2em;
border-left: 4px solid var(--accent);
background: var(--accent-soft);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
color: var(--text);
}
.markdown-section hr {
border: 0;
border-top: 1px solid var(--border);
margin: 2.5em 0;
}
/* Lists */
.markdown-section ul,
.markdown-section ol {
padding-left: 1.5em;
}
.markdown-section li {
margin: 0.25em 0;
}
.markdown-section li::marker {
color: var(--accent);
}
/* Code */
.markdown-section code {
font-family: var(--font-mono);
font-size: 0.88em;
padding: 2px 6px;
background: var(--bg-inline-code);
border-radius: 4px;
color: var(--text);
}
.markdown-section pre {
margin: 1.2em 0;
padding: 0;
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
}
.markdown-section pre > code {
display: block;
padding: 16px 18px;
background: transparent;
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.6;
overflow-x: auto;
border-radius: 0;
}
.markdown-section pre[data-lang]::before {
content: attr(data-lang);
display: block;
padding: 6px 14px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-subtle);
background: var(--bg-elev);
border-bottom: 1px solid var(--border);
}
/* ---------- Prism syntax highlighting (light mode) ---------- */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #8a7f72;
font-style: italic;
}
.token.punctuation {
color: #5c524a;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #b4651f;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #6a8a3a;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #a82557;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #c8396e;
font-weight: 600;
}
.token.function,
.token.class-name {
color: #2a6a9a;
}
.token.regex,
.token.important,
.token.variable {
color: #b4651f;
}
.token.important,
.token.bold {
font-weight: 700;
}
.token.italic {
font-style: italic;
}
/* ---------- Prism syntax highlighting (dark mode) ---------- */
@media (prefers-color-scheme: dark) {
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #8a7e6f;
}
.token.punctuation {
color: #b3a796;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #e3a36a;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a8c97a;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #ff9ec0;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #ff7aa8;
}
.token.function,
.token.class-name {
color: #7bb6e0;
}
.token.regex,
.token.important,
.token.variable {
color: #e3a36a;
}
}
/* Tables */
.markdown-section table {
width: 100%;
margin: 1.2em 0;
border-collapse: collapse;
display: block;
overflow-x: auto;
}
.markdown-section th,
.markdown-section td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.markdown-section th {
font-weight: 700;
background: var(--bg-sidebar);
}
.markdown-section tr:hover td {
background: var(--accent-soft);
}
/* Images */
.markdown-section img {
max-width: 100%;
border-radius: var(--radius);
}
/* ---------- Homepage extras ---------- */
.hero-badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 1.5em;
}
.hero-badges .badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
font-size: 12.5px;
font-weight: 700;
background: var(--accent-soft);
color: var(--accent);
border-radius: 999px;
border-bottom: 0;
}
.hero-badges .badge:hover {
background: var(--accent);
color: var(--accent-contrast);
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin: 1.6em 0 2em;
padding: 0;
list-style: none;
}
.feature-grid li {
margin: 0;
padding: 18px 20px;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
transition: transform 0.15s ease, box-shadow 0.15s ease,
border-color 0.15s ease;
}
.feature-grid li:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
border-color: var(--border-strong);
}
.feature-grid li::marker {
content: "";
}
.feature-grid .feature-title {
display: block;
font-weight: 700;
font-size: 15.5px;
margin-bottom: 4px;
color: var(--text);
}
.feature-grid .feature-icon {
display: inline-block;
margin-right: 6px;
}
.feature-grid .feature-body {
font-size: 14px;
color: var(--text-muted);
line-height: 1.5;
}
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 1.5em 0 2em;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
font-family: var(--font-sans);
font-weight: 700;
font-size: 15px;
border-radius: var(--radius-sm);
border: 1px solid transparent;
border-bottom: 1px solid transparent !important;
cursor: pointer;
transition: transform 0.08s ease, box-shadow 0.15s ease,
background 0.15s ease, color 0.15s ease;
}
.btn-primary {
background: var(--accent);
color: var(--accent-contrast);
}
.btn-primary:hover {
background: var(--accent-hover);
color: var(--accent-contrast);
transform: translateY(-1px);
box-shadow: var(--shadow);
}
.btn-secondary {
background: transparent;
color: var(--text);
border-color: var(--border-strong) !important;
}
.btn-secondary:hover {
background: var(--bg-elev);
color: var(--text);
transform: translateY(-1px);
box-shadow: var(--shadow);
}
.version-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 8px;
padding: 0;
list-style: none;
margin: 1em 0 2em;
}
.version-grid li {
margin: 0;
}
.version-grid li::marker {
content: "";
}
.version-grid a {
display: block;
padding: 8px 10px;
text-align: center;
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-muted);
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
border-bottom: 1px solid var(--border);
}
.version-grid a:hover {
color: var(--accent);
border-color: var(--accent);
background: var(--accent-soft);
}
.version-grid .current a {
color: var(--accent);
border-color: var(--accent);
font-weight: 700;
background: var(--accent-soft);
}
.book-promo {
display: flex;
gap: 20px;
align-items: center;
padding: 20px;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
margin: 1.5em 0;
box-shadow: var(--shadow);
}
.book-promo img {
width: 110px;
flex-shrink: 0;
border-radius: 4px;
}
.book-promo .book-body {
flex: 1;
}
.book-promo h4 {
margin: 0 0 4px !important;
font-size: 1.05rem !important;
}
.book-promo p {
margin: 4px 0 !important;
font-size: 14px;
color: var(--text-muted);
}
/* ---------- Cover page ---------- */
section.cover {
/* Hidden by default — docsify adds .show only on the homepage route. */
display: none;
position: relative;
min-height: 100dvh;
align-items: center;
justify-content: center;
padding: 40px 24px;
margin-left: var(--sidebar-width);
/* !important overrides the inline gradient docsify injects on
the cover section. */
background:
radial-gradient(
ellipse at 85% 15%,
var(--accent-soft),
transparent 45%
),
radial-gradient(
ellipse at 15% 95%,
rgba(255, 165, 100, 0.08),
transparent 45%
),
var(--bg) !important;
}
section.cover.show {
display: flex;
}
section.cover .cover-main {
max-width: 760px;
text-align: center;
}
section.cover .cover-main > p:last-child a {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 8px 6px 0;
padding: 12px 22px;
font-weight: 700;
font-size: 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-strong);
color: var(--text);
background: var(--bg-elev);
box-shadow: var(--shadow);
border-bottom: 1px solid var(--border-strong);
transition: transform 0.1s ease, box-shadow 0.15s ease;
}
section.cover .cover-main > p:last-child a:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
section.cover .cover-main > p:last-child a:first-child {
background: var(--accent);
color: var(--accent-contrast);
border-color: var(--accent);
}
section.cover .cover-main > p:last-child a:first-child:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
color: var(--accent-contrast);
}
section.cover h1 {
margin: 0 0 0.15em;
font-size: 4rem;
letter-spacing: -0.03em;
font-weight: 700;
line-height: 1;
}
section.cover h1 a {
color: var(--accent);
border-bottom: 0;
position: relative;
display: inline-block;
/* Halo ensures edges stay legible against the gradient backdrop */
text-shadow:
0 0 1px var(--bg),
0 0 12px var(--bg),
0 2px 24px rgba(0, 0, 0, 0.08);
}
section.cover h1 small {
/* Absolute so it hangs off the right edge without affecting the
centering of "Taffy". line-height:1 on both ensures bottom:0
== baseline alignment in the same font family. */
position: absolute;
left: calc(100% + 0.15em);
bottom: 0;
line-height: 1;
font-size: 1rem;
color: var(--text-subtle);
font-weight: 400;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
text-shadow: none;
}
section.cover blockquote {
margin: 1em 0 1.5em;
padding: 0;
border: 0;
background: transparent;
}
section.cover blockquote p {
font-size: 1.35rem;
color: var(--text-muted);
font-weight: 400;
line-height: 1.45;
margin: 0;
}
section.cover .cover-main > p:nth-last-child(2) {
font-size: 14px;
color: var(--text-subtle);
margin-bottom: 1.5em;
}
.cover-main img {
max-width: 160px;
margin-bottom: 1em;
}
/* ---------- Pagination (next/prev) ---------- */
.docsify-pagination-container {
max-width: var(--content-width);
margin: 0 auto;
padding: 0 40px 80px;
}
.pagination-item {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-elev);
}
.pagination-item-title {
color: var(--accent) !important;
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
:root {
--sidebar-width: 260px;
}
.sidebar {
transform: translateX(-100%);
box-shadow: var(--shadow-lg);
}
body.close .sidebar {
transform: translateX(0);
}
.content {
margin-left: 0;
}
.sidebar-toggle {
display: flex;
}
.markdown-section {
padding: 72px 22px 80px;
}
.docsify-pagination-container {
padding: 0 22px 80px;
}
section.cover {
margin-left: 0;
}
section.cover h1 {
font-size: 3rem;
}
.book-promo {
flex-direction: column;
text-align: center;
}
}
/* Docsify loading state */
.app-name-link img.logo {
display: none;
}
/* ---------- GitHub corner ---------- */
.github-corner {
position: fixed;
top: 0;
right: 0;
z-index: 20;
border-bottom: 0;
text-decoration: none;
}
.github-corner svg {
width: 72px;
height: 72px;
color: var(--bg);
fill: var(--accent);
}
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%, 100% { transform: rotate(0); }
20%, 60% { transform: rotate(-25deg); }
40%, 80% { transform: rotate(10deg); }
}
@media (max-width: 900px) {
.github-corner svg {
width: 56px;
height: 56px;
}
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
</style>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app">Loading…</div>
<aside
class="book"
style="position: absolute; top: 50px; right: 20px; width: 160px"
>
<br />Need to better understand REST fundamentals? <br /><a
href="http://www.restassuredbook.com"
>
<img
src="assets/book-3d.png"
width="100"
alt="Picture of book cover art for Adam's book: REST Assured, A Pragmatic Approach to API Design"
/>
<br />I wrote a book for you.
</a>
</aside>
<script> <script>
window.$docsify = { window.$docsify = {
name: "Taffy Docs", name: "Taffy",
repo: "atuttle/Taffy", repo: "atuttle/Taffy",
maxLevel: 4, loadSidebar: false,
coverpage: true,
onlyCover: false,
maxLevel: 3,
subMaxLevel: 2,
auto2top: true,
search: {
maxAge: 86400000,
paths: "auto",
placeholder: "Search docs…",
noData: "No results.",
depth: 4,
},
}; };
</script> </script>
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script> <script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-markup-templating.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-javascript.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-cfscript.min.js"></script>
</body> </body>
</html> </html>
+136 -22
View File
@@ -1,32 +1,146 @@
<img alt="Taffy Logo" src="https://taffy.io/images/logo.png" style="max-width: 300px" /> <p class="hero-badges">
<a class="badge" href="https://github.com/atuttle/Taffy"><span>★</span> GitHub</a>
<a class="badge" href="https://github.com/atuttle/Taffy/releases"><span>⬇</span> Releases</a>
<a class="badge" href="https://cfml-slack.herokuapp.com/"><span>💬</span> #taffy on CFML Slack</a>
<a class="badge" href="https://github.com/atuttle/Taffy/blob/master/LICENSE"><span>⚖</span> Apache-2.0</a>
</p>
# Taffy # Write REST APIs in CFML — without the boilerplate.
The REST Web Service framework for ColdFusion and Lucee **Taffy** is a full-featured REST framework for ColdFusion and Lucee. Drop a
CFC into your `/resources/` folder, add a URI annotation, and you have an API.
Content negotiation, caching hooks, CORS, JSONP, an interactive dashboard,
OpenAPI JSON, and custom serializers — all included.
- [Download Taffy 3.3.0.zip](https://github.com/atuttle/Taffy/archive/v3.3.0.zip) <p class="cta-row">
- [GitHub](https://github.com/atuttle/Taffy) <a class="btn btn-primary" href="https://github.com/atuttle/Taffy/archive/v4.0.0.zip">Download v4.0.0</a>
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/) <a class="btn btn-secondary" href="#/?id=quickstart-your-first-api">Quickstart &rarr;</a>
<a class="btn btn-secondary" href="#/4.0.0">Read the Docs</a>
</p>
# Documentation ## Why Taffy?
Nobody has time to keep every framework and every dependency up to date. Sometimes you want to reference the old docs! That's why Taffy tries really hard to maintain our old docs and carry them forward with us. Below is a list of all version-specific Taffy docs releases. <ul class="feature-grid">
<li>
<span class="feature-title"><span class="feature-icon">⚡</span>One-file APIs</span>
<span class="feature-body">A working API fits in a tweet. Seriously — inheritance and convention do the heavy lifting.</span>
</li>
<li>
<span class="feature-title"><span class="feature-icon">🧭</span>Convention over config</span>
<span class="feature-body">URI lives on the CFC. HTTP verbs map to CFC methods. That's the contract.</span>
</li>
<li>
<span class="feature-title"><span class="feature-icon">🎛</span>Built-in dashboard</span>
<span class="feature-body">Interactive API explorer at your index.cfm. Try every endpoint from a browser without writing a client.</span>
</li>
<li>
<span class="feature-title"><span class="feature-icon">🧬</span>Content negotiation</span>
<span class="feature-body">JSON, XML, JSONP, custom serializers — chosen by <code>Accept</code> header or extension. You pick the defaults.</span>
</li>
<li>
<span class="feature-title"><span class="feature-icon">📘</span>OpenAPI out of the box</span>
<span class="feature-body">Auto-generated OpenAPI/Swagger JSON for every route, new in v4. Point your favorite tooling at it and go.</span>
</li>
<li>
<span class="feature-title"><span class="feature-icon">🧱</span>Pluggable everything</span>
<span class="feature-body">Bring your own bean factory, serializer, status reporter, or caching strategy. The hooks are there.</span>
</li>
</ul>
## Quickstart: Your First API
Three files. That's the whole API.
**Application.cfc**
```cfscript
component extends="taffy.core.api" {}
```
**index.cfm**
```
<!-- intentionally empty -->
```
**/resources/hello.cfc**
```cfscript
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return rep( [ "hello", "world" ] );
}
}
```
Point a browser at `index.cfm` and you get the Taffy dashboard. Hit
`/hello` and you get JSON. Add a `post()` method — that's your POST handler.
Add `taffy_uri="/hello/{name}"` — now `name` is an argument.
[Read the full Getting Started guide &rarr;](https://github.com/atuttle/Taffy/wiki/Getting-Started)
## Current Release
> **Taffy 4.0.0** is the latest stable release.
> [Release notes](https://github.com/atuttle/Taffy/releases/tag/v4.0.0) ·
> [Download](https://github.com/atuttle/Taffy/archive/v4.0.0.zip) ·
> [Docs](4.0.0.md)
## All Versions
Taffy keeps every version's docs online forever — because nobody has time to
keep every framework and every dependency up to date. If you're still on an
older version, the docs that shipped with it still work.
<!--new_docs_links_here--> <!--new_docs_links_here-->
- [v3.5.0](3.5.0.md) ### Taffy 4.x
- [v3.4.0](3.4.0.md)
- [v3.3.0](3.3.0.md)
- [v3.2.0](3.2.0.md)
- [v3.1.0](3.1.0.md)
- [v3.0.0](3.0.0.md)
- [v2.2.4](2.2.4.md)
- [v2.2.3](2.2.3.md)
- [v2.2.0](2.2.0.md)
- [v2.1.0](2.1.0.md)
- [v2.0.1](2.0.1.md)
- [v2.0.0](2.0.0.md)
# Other Resources <ul class="version-grid">
<li class="current"><a href="#/4.0.0">v4.0.0</a></li>
</ul>
In 2012 I presented a comparison of REST frameworks for CFML at the cf.Objective() conference. [This is all of the source code I used to make that comparison, as well as my slides from the talk.](https://github.com/atuttle/CF-REST-Comparison#coldfusion-rest-comparison) ### Taffy 3.x
<ul class="version-grid">
<li><a href="#/3.7.1">v3.7.1</a></li>
<li><a href="#/3.7.0">v3.7.0</a></li>
<li><a href="#/3.6.0">v3.6.0</a></li>
<li><a href="#/3.5.0">v3.5.0</a></li>
<li><a href="#/3.4.0">v3.4.0</a></li>
<li><a href="#/3.3.0">v3.3.0</a></li>
<li><a href="#/3.2.0">v3.2.0</a></li>
<li><a href="#/3.1.0">v3.1.0</a></li>
<li><a href="#/3.0.0">v3.0.0</a></li>
</ul>
### Taffy 2.x
<ul class="version-grid">
<li><a href="#/2.2.4">v2.2.4</a></li>
<li><a href="#/2.2.3">v2.2.3</a></li>
<li><a href="#/2.2.0">v2.2.0</a></li>
<li><a href="#/2.1.0">v2.1.0</a></li>
<li><a href="#/2.0.1">v2.0.1</a></li>
<li><a href="#/2.0.0">v2.0.0</a></li>
</ul>
## Community
- 💬 Join **#taffy** on the [CFML Slack](https://cfml-slack.net/)
- 🐛 [Report issues on GitHub](https://github.com/atuttle/Taffy/issues)
- 🙋 [Contribute](https://github.com/atuttle/Taffy/blob/master/CONTRIBUTING.md)
- 📜 [Changelog](https://github.com/atuttle/Taffy/releases)
## Other Resources
<div class="book-promo">
<a href="http://www.restassuredbook.com"><img src="assets/book-3d.png" alt="REST Assured — A Pragmatic Approach to API Design" /></a>
<div class="book-body">
<h4>Need to level up on REST fundamentals?</h4>
<p>Taffy's author wrote a book on API design. <em>REST Assured — A Pragmatic Approach to API Design</em> covers the parts every framework leaves out.</p>
<p><a href="http://www.restassuredbook.com">Get the book &rarr;</a></p>
</div>
</div>
-3
View File
@@ -1,3 +0,0 @@
Files in the /lib folder exist solely for the purpose of running the tests.
You do not need to keep them for your installation. You can delete everything
except /core and /bonus, but I would recommend you keep /examples around too.
Binary file not shown.
Binary file not shown.
+12 -19
View File
@@ -1,21 +1,14 @@
{ {
"name": "cfml-taffy", "name": "cfml-taffy",
"scripts": { "version": "3.8.0",
"less": "lessc dashboard/dash.less --clean-css=\"--s1 --advanced\" > dashboard/dash.css" "author": "Adam Tuttle <adamtuttlecodes@gmail.com>",
}, "license": "MIT",
"repository": { "homepage": "https://taffy.io",
"type": "git", "repository": {
"url": "git+https://github.com/atuttle/Taffy.git" "type": "git",
}, "url": "git+https://github.com/atuttle/Taffy.git"
"author": "Adam Tuttle <adamtuttlecodes@gmail.com>", },
"license": "MIT", "bugs": {
"bugs": { "url": "https://github.com/atuttle/Taffy/issues"
"url": "https://github.com/atuttle/Taffy/issues" }
},
"homepage": "https://taffy.io",
"devDependencies": {
"less": "^4.1.1",
"less-plugin-clean-css": "^1.5.1"
},
"version": "3.6.0"
} }
+12
View File
@@ -0,0 +1,12 @@
{
"name":"taffy-lucee5",
"web":{
"http":{
"port":8085
}
},
"app":{
"cfengine":"lucee@5"
},
"openBrowser":false
}
+12
View File
@@ -0,0 +1,12 @@
{
"name":"taffy-lucee6",
"web":{
"http":{
"port":8086
}
},
"app":{
"cfengine":"lucee@6"
},
"openBrowser":false
}
+12
View File
@@ -0,0 +1,12 @@
{
"name":"taffy-lucee7",
"web":{
"http":{
"port":8087
}
},
"app":{
"cfengine":"lucee@7"
},
"openBrowser":false
}
-5
View File
@@ -1,5 +0,0 @@
{
"app":{
"cfengine":""
}
}
+42 -76
View File
@@ -1,80 +1,46 @@
<cfcomponent extends="taffy.core.api" output="false"> component {
<cfscript>
this.name = "Taffy_testSuite";
this.dirPath = getDirectoryFromPath(getCurrentTemplatePath());
this.mappings["/resources"] = this.dirPath & "resources/";
this.mappings["/mxunit"] = this.dirPath & "testbox/system/compat/";
this.mappings["/testbox"] = this.dirPath & "testbox/";
this.mappings["/bugLog"] = this.dirPath & "BugLogHQ/";
this.mappings["/Hoth"] = this.dirPath & "Hoth/";
this.mappings["/di1"] = this.dirPath & "di1/";
this.system = createObject("java", "java.lang.System"); this.name = "TaffyTestHarness_" & hash(getCurrentTemplatePath());
this.datasources["bugLog"] = { this.sessionManagement = false;
class: (structKeyExists(server, "lucee")) ? 'org.gjt.mm.mysql.Driver' : 'com.mysql.jdbc.Driver',
connectionString: 'jdbc:mysql://localhost:3306/buglog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT&useLegacyDatetimeCode=false', // Set up mappings for tests
url: 'jdbc:mysql://localhost:3306/buglog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT&useLegacyDatetimeCode=false', variables.testsPath = getDirectoryFromPath(getCurrentTemplatePath());
username: this.system.getenv("DB_USER"), // Strip trailing slash before calling getDirectoryFromPath to reliably get parent dir across engines
password: this.system.getenv("DB_PASS"), variables.rootPath = getDirectoryFromPath(left(variables.testsPath, len(variables.testsPath) - 1));
driver: "other"
this.mappings["/taffy"] = variables.rootPath;
this.mappings["/testbox"] = variables.testsPath & "testbox";
this.mappings["/tests"] = variables.testsPath;
this.mappings["/resources"] = variables.testsPath & "resources";
// Also set component paths for Lucee 6 compatibility
this.componentPaths = [
{
"physical": variables.rootPath,
"archive": "",
"primary": "physical",
"inspectTemplate": "always"
}
];
function onApplicationStart() {
// Initialize minimal Taffy context for tests that need it
application._taffy = {
settings: {
serializer: "core.nativeJsonSerializer",
noDataSends204NoContent: false
},
factory: new core.factory(),
compat: {
queryToArray: "missing",
queryToStruct: "missing"
}
}; };
return true;
}
function onRequestStart(targetPath) {
return true;
}
variables.framework = {}; }
variables.framework.disableDashboard = false;
variables.framework.reloadKey = "reload";
variables.framework.unhandledPaths = "/Taffy/tests/someFolder,/Taffy/tests/tests,/tests/someFolder,/tests/tests,BugLogHQ";
variables.framework.serializer = "nativeJsonSerializer";
variables.framework.useEtags = true;
variables.framework.globalHeaders = {};
variables.framework.globalHeaders["x-foo-globalheader"] = "snafu";
variables.framework.exposeHeaders = true;
variables.framework.environments = {};
variables.framework.environments.test = {};
variables.framework.environments.test.reloadPassword = 'dontpanic';
function getCacheKey(cfc, requestArguments, matchedURI) {
if(structKeyExists(requestArguments, "default")) {
return super.getCacheKey(argumentCollection = arguments);
}
return lCase(arguments.cfc & "_" & listSort(structKeyList(arguments.requestArguments), "textnocase"));
}
function getEnvironment(){
return "test";
}
function onRequest(string target) {
if (arguments.target contains "/BugLogHQ/") {
return true;
} else {
super.onRequest(arguments.target);
}
}
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers, methodMetadata, matchedURI) {
var local = {};
//pass data into a resource by modifying requestArguments
if (structKeyExists(arguments.requestArguments, "hulk") and arguments.requestArguments.hulk eq "smash"){
arguments.requestArguments.dataFromOTR = "who let the hulk out?!";
}
//get basic auth data, if any, and pass it into the resources
local.credentials = getBasicAuthCredentials();
arguments.requestArguments.username = local.credentials.username;
arguments.requestArguments.password = local.credentials.password;
if (structKeyExists(arguments.requestArguments, "refuse") and arguments.requestArguments.refuse)
{
return noData().withStatus(405);
}
else
{
return true;
}
}
</cfscript>
</cfcomponent>
+55
View File
@@ -0,0 +1,55 @@
component {
/**
* Returns a sample query for testing queryToArray/queryToStruct
*/
function getSampleQuery() {
return queryNew(
"id,name,email,active",
"integer,varchar,varchar,bit",
[
{ id: 1, name: "John Doe", email: "john@example.com", active: true },
{ id: 2, name: "Jane Smith", email: "jane@example.com", active: true },
{ id: 3, name: "Bob Wilson", email: "bob@example.com", active: false }
]
);
}
/**
* Returns a single-row query for testing queryToStruct
*/
function getSingleRowQuery() {
return queryNew(
"id,name,email",
"integer,varchar,varchar",
[{ id: 1, name: "Test User", email: "test@example.com" }]
);
}
/**
* Returns sample struct data
*/
function getSampleStruct() {
return {
id: 123,
name: "Test Item",
tags: ["tag1", "tag2"],
metadata: {
created: now(),
version: "1.0"
}
};
}
/**
* Returns sample array data
*/
function getSampleArray() {
return [
{ id: 1, value: "first" },
{ id: 2, value: "second" },
{ id: 3, value: "third" }
];
}
}
+18
View File
@@ -0,0 +1,18 @@
component {
/**
* Walks the metadata chain to check if an object extends a given class.
* Works around Lucee's isInstanceOf() bug with mapped paths.
*/
public boolean function extendsClass(required any obj, required string className) {
if (!isObject(arguments.obj)) return false;
var meta = getMetadata(arguments.obj);
while (structKeyExists(meta, "name")) {
if (listLast(meta.name, ".") == listLast(arguments.className, ".")) return true;
if (!structKeyExists(meta, "extends")) break;
meta = meta.extends;
}
return false;
}
}
View File
-147
View File
@@ -1,147 +0,0 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 01/12/2011
HothReporter offers a simple Web UI to report the errors observed by Hoth.
// Modifications :---------------------------------------------------------
*/
component
name='HothReporter'
accessors=false
output="false"
{
public Hoth.HothReporter function init (HothConfig)
{
// If a config object was not provided we
// will use our default.
variables.Config = (structKeyExists(arguments, 'HothConfig'))
? arguments.HothConfig
: new Hoth.config.HothConfig();
VARIABLES._NAME = 'Hoth_' & variables.Config.getApplicationName();
variables.exceptionKeys = ['detail','type','tagcontext','stacktrace','message'];// Required exception keys
variables.logPathIsRelative = variables.Config.getLogPathIsRelative();
variables.paths.LogPath = variables.Config.getLogPathExpanded(); // Get the root location for our logging.
variables.paths.Exceptions = variables.Config.getPath('exceptions'); // Track the unique exceptions.
variables.paths.Incidents = variables.Config.getPath('incidents'); // Track the hits per exception.
variables.paths.Report = variables.Config.getPath('exceptionReport'); // The actual report
variables.paths.Activity = variables.Config.getPath('exceptionReportActivity'); // Track when we save things. Helps understand volume.
//variables.paths.Index = variables.Config.getPath('exceptionIndex'); // Tracks the exception keys to prevent duplication
return this;
}
/** Quick report. Really a work in process.
* @exception Accepts a hash value or 'all'
**/
public struct function report (required string exception)
{
if (arguments.exception=='all')
{
return generateExceptionIndex();
} else {
local.filepath = variables.paths.Exceptions & '/' & arguments.exception;
local.exception = (fileExists(local.filepath))
? fileRead(local.filepath)
: serializeJSON ({'message'="That report no longer exists."});
return deserializeJSON (local.exception);
}
}
/** Quick report. Really a work in process.
* @exception Accepts a hash value or 'all'
**/
public array function delete (required string exception)
{
local.response = [];
if (arguments.exception == 'all')
{
lock name=VARIABLES._NAME timeout=variables.Config.getTimeToLock() type="exclusive" {
directoryDelete(variables.paths.Exceptions, true);
directoryDelete(variables.paths.Incidents, true);
directoryCreate(variables.paths.Exceptions);
directoryCreate(variables.paths.Incidents);
}
} else { local.exceptionPath = variables.paths.Exceptions & '/' & arguments.exception;
local.incidentPath = variables.paths.Incidents & '/' & arguments.exception;
local.response = [];
if (fileExists(local.exceptionPath))
{
fileDelete(local.exceptionPath);
arrayAppend(local.response, "Exception file deleted.");
} else {
arrayAppend(local.response, "Exception file did not exist!");
}
if (fileExists(local.incidentPath))
{
fileDelete(local.incidentPath);
arrayAppend(local.response, "Incident record file deleted.");
} else {
arrayAppend(local.response, "Incident record file not exist!");
}
return local.response;
}
}
/** Return the report's view **/
public string function getReportView () {
local.view = fileRead(expandPath('/Hoth') & '/views/report.html');
// Replace the Hoth URL
return replaceNoCase(
local.view
,'${HOTH_REPORT_URL}'
,variables.Config.getHothReportURL());
}
// -------------------------------------------------------------------------
private struct function generateExceptionIndex() {
// Read our file system
local.exceptions = directoryList (variables.paths.Exceptions,false);
local.incidents = directoryList (variables.paths.Exceptions,false);
local.report = {};
for (i=1;i LTE ArrayLen(local.exceptions);i=i+1) {
local.instance = {};
local.instance.filename =
listLast(local.exceptions[i],'\/');
if (left(local.instance.filename, 1) != '_')
{
//local.instance.exceptionDetail =
//fileRead (local.exceptions[i]);
if (!fileExists(variables.paths.Incidents & '/' & local.instance.filename))
{
local.instances = '';
} else {
local.instances =
fileRead(variables.paths.Incidents & '/' & local.instance.filename);
}
local.instance.incidentcount = listLen(local.instances,chr(10));
// Save our report
local.report[local.instance.filename] = local.instance;
}
}
return local.report;
}
}
-264
View File
@@ -1,264 +0,0 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
HothTracker is responsible for accepting a exception, tracking the frequency
of the exception and alerting developers for new, unique exceptions.
// Modifications :---------------------------------------------------------
Modified : 12/13/2010 9:52:41 AM by Aaron Greenlee.
- Now supporting ColdBox 3.0 RC1
*/
component
name='HothTracker'
accessors=false
{
public Hoth.HothTracker function init (HothConfig) {
// If a config object was not provided we
// will use our default.
variables.Config = (structKeyExists(arguments, 'HothConfig'))
? arguments.HothConfig
: new Hoth.object.HothConfig();
VARIABLES._NAME = 'Hoth_' & variables.Config.getApplicationName();
variables.exceptionKeys = ['detail','type','tagcontext','stacktrace','message']; // Required exception keys
variables.paths.LogPath = variables.Config.getLogPathExpanded(); // Get the root location for our logging.
variables.paths.Exceptions = variables.Config.getPath('exceptions'); // Track the unique exceptions.
variables.paths.Incidents = variables.Config.getPath('incidents'); // Track the hits per exception.
variables.paths.Report = variables.Config.getPath('exceptionReport'); // The actual report
variables.paths.Activity = variables.Config.getPath('exceptionReportActivity'); // Track when we save things. Helps understand volume.
//variables.paths.Index = variables.Config.getPath('exceptionIndex'); // Tracks the exception keys to prevent duplication
verifyDirectoryStructure();
try {
// For the life of this HothTracker, keep the ApplicationManager.
variables.HothApplicationManager = new Hoth.object.HothApplicationManager(HothConfig);
// Because the entire application db file is locked, we only want to
// learn about this application when the HothTracker is created. The
// actual amount of work done within the manager is small, but, it is
// does block all other processes for all other Hoth instances, so, we
// want to reduce calls to it to this constructor.
variables.HothApplicationManager.learnApplication(arguments.HothConfig);
} catch (any e) {
//writeDump(e);abort;
}
return this;
}
/** Track an exception.
@ExceptionStructure A ColdFusion cfcatch or a supported object from a Framework or Application. */
public boolean function track (any Exception) {
local.ExceptionStructure = parseException(arguments.Exception);
// If we did not parse what we are supposed to
// track, we will abort.
if (!local.ExceptionStructure.validException)
return false;
// ------------------------------------------------------------------------------
try {
// ------------------------------------------------------------------------------
local.e = {
detail = structKeyExists(local.ExceptionStructure,'detail') ? local.ExceptionStructure.detail : '_noDetail'
,message = structKeyExists(local.ExceptionStructure,'message') ? local.ExceptionStructure.message : '_noMessage'
,stack = structKeyExists(local.ExceptionStructure,'stacktrace') ? local.ExceptionStructure.stacktrace : '_no_stacktrace'
,context = structKeyExists(local.ExceptionStructure,'tagcontext') ? local.ExceptionStructure.tagcontext : '_no_tagcontext'
,format = structKeyExists(local.ExceptionStructure,'format') ? local.ExceptionStructure.format: '_no_format'
,url = CGI.HTTP_HOST & CGI.path_info
,client = CGI.HTTP_USER_AGENT
};
// Generate JSON for hashing
local.json = {};
local.k = '';
for(local.k in local.e)
local.json[k] = serializeJSON(local.e[local.k]);
// Hash a unique key for the content of each property within the exception
local.index = {};
local.index.stack = ( len(local.e.stack) > 0) ? hash(lcase(local.e.stack),'SHA') : '_no_stack';
local.index.key = lcase(local.index.stack);
local.saveDetails = false;
// Index the exception, count occurances and save details.
// Lock is unique to the exception.
lock name=local.index.key timeout=variables.Config.getTimeToLock() type="exclusive" {
local.filename = local.index.key & '.log';
local.exceptionFile = variables.paths.Exceptions & '/' & local.filename;
local.incidentsFile = variables.paths.Incidents & '/' & local.filename;
local.exceptionIsKnown = fileExists(local.exceptionFile);
if (!local.exceptionIsKnown)
fileWrite(local.exceptionFile ,serializeJSON(local.e),'UTF-8');
// Create an incident if the file does not exist
if (!fileExists(local.incidentsFile)) {
fileWrite(local.incidentsFile ,now() & '#chr(13)#','UTF-8');
} else {
local.file = fileOpen(local.incidentsFile,'append','utf-8');
fileWriteLine(local.file, now() & '|' & local.e.url );
fileClose(local.file);
}
}
// Outside the lock, send mail if requested
if (!local.exceptionIsKnown && variables.Config.getEmailNewExceptions() ) {
local.INetAddress = createObject( 'java', 'java.net.InetAddress' );
local.url = (len(CGI.QUERY_STRING) > 0)
? CGI.http_host & CGI.path_info & '?' & Cgi.QUERY_STRING
: CGI.http_host & CGI.path_info;
local.emailBody = [
"Hoth tracked a new exception (" & local.index.key & ")."
,"Message: " & local.e.message
,"Machine Name: " & local.INetAddress.getLocalHost().getHostName()
,"Address: " & local.url
];
local.Mail = new Mail( subject='Hoth Exception (' & variables.Config.getApplicationName() & ') ' & local.index.key
,to=variables.Config.getEmailNewExceptionsTo()
,from=variables.Config.getEmailNewExceptionsFrom());
// Attach the file
if ( variables.Config.getEmailNewExceptionsFile() ) {
local.Mail.addParam(file=local.exceptionFile);
ArrayAppend( local.emailBody, "To view the attached exception info, copy and paste into FireBug's console (x = exception) and press CRTL+Enter." );
}
// Show exception as HTML inline?
if ( variables.Config.getEmailExceptionsAsHTML() ) {
local.Mail.setType( "html" );
savecontent variable="local.emailBody" {
writeOutput( arrayToList( local.emailBody, "<br />" ) );
writeOutput( "<br />Exception Details:<br />");
writeDump( local.e );
}
local.Mail.setBody( local.emailBody );
}
else {
local.Mail.setBody( arrayToList( local.emailBody, "#chr(10)##chr(13)#" ) );
}
local.mail.Send();
}
// ------------------------------------------------------------------------------
} catch (any e) {
return false;
}
// ------------------------------------------------------------------------------
return true;
}
// Private Methods Follow -------------------------------------------------------
/** Parse an exception provided by a framework or supported application.
Hoth is easy to support. Just provide a Struct with at least the following keys:
'detail,type,tagContext,StackTrace,Message'
Or an object with the same information that Hoth can extract. */
private struct function parseException(any Exception) {
local.result = { validException = false };
// Return the meta data for our passed exception
local.md = getMetaData(Exception);
// Inspect the class tree to understand what type of object
// we are working with.
local.classTree = GetClassHeirarchy(local.md);
// Was our object built with ColdFusion? If so, it is custom.
// If not, the object is considered to be native and generated by CF.
local.exceptionType = (
listContainsNoCase(local.classTree, 'coldfusion.runtime.AttributeCollection', ' ')
) ? 'Custom' : 'Native';
if (local.exceptionType == 'Native')
{
local.result.detail = (structKeyExists(arguments.Exception, 'detail')) ? arguments.Exception.detail : 'undefined';
local.result.message = (structKeyExists(arguments.Exception, 'message')) ? arguments.Exception.message : 'undefined';
local.result.stacktrace = (structKeyExists(arguments.Exception, 'stacktrace')) ? arguments.Exception.stacktrace : 'undefined';
local.result.tagcontext = (structKeyExists(arguments.Exception, 'tagContext')) ? arguments.Exception.tagContext : 'undefined';
local.result.validException = true;
local.result.format = 'Native';
// ADDED by Benoit Hediard to get real detail and message in FW1
if (local.result.message == "Event handler exception." && structKeyExists(arguments.Exception, "Cause")) {
local.result.detail = (structKeyExists(arguments.Exception.Cause, 'detail')) ? arguments.Exception.Cause.detail : 'undefined';
local.result.message = (structKeyExists(arguments.Exception.Cause, 'message')) ? arguments.Exception.Cause.message : 'undefined';
};
return local.result;
} else {
//detail,type,tagcontext,stacktrace,message
switch(local.md.fullname) {
// ColdBox Excepiont
case 'coldbox.system.beans.ExceptionBean' :
case 'coldbox.system.web.context.ExceptionBean' :
local.result.detail = arguments.Exception.getDetail();
local.result.message = arguments.Exception.getMessage();
local.result.stacktrace = arguments.Exception.getStackTrace();
local.result.tagcontext = arguments.Exception.getTagContext();
local.result.validException = true;
local.result.format = 'ColdBox';
break;
// Unknown Exception Passed
default :
local.result.validException = false;
local.result.format = 'Unknown';
break;
/*case 'MachII.util.Exception' :
local.result = arguments.Exception.getCaughtException();
local.result.validException = true;
break;*/
}
}
return local.result;
}
/** Inspect the class heirarchy to return a string showing the class tree.
Original code is borrowed from Dominic Watson at the following address.
Rewritten for for CF9+/Railo features.
http://fusion.dominicwatson.co.uk/2007/09/coldfusion-objects-are-java-objects.html **/
private function GetClassHeirarchy(obj) {
local.thisClass = obj.GetClass();
local.sReturn = thisClass.GetName();
do{
local.thisClass = local.thisClass.GetSuperClass();
local.sReturn &= " " & thisClass.GetName();
} while(CompareNoCase(thisClass.GetName(), 'java.lang.Object'));
return local.sReturn;
}
/** Verify the desired directory structure exsits. */
private void function verifyDirectoryStructure() {
// Verify our index diectory exists
/* Ensure our directory structure is as expected. */
lock name=VARIABLES._NAME timeout=variables.Config.getTimeToLock() type="exclusive" {
if (!directoryExists(variables.paths.Exceptions)) {
directoryCreate(variables.paths.Exceptions);
fileWrite(variables.paths.Exceptions & '/_readme.txt','Hoth: The files within this directory contain the complete details for each unique exception.');
}
if (!directoryExists(variables.paths.Incidents)) {
directoryCreate(variables.paths.Incidents);
fileWrite(variables.paths.Incidents & '/_readme.txt','Hoth: The files within this directory contain the details about the volume of errors for each unique exception.');
}
}
return;
}
}
-41
View File
@@ -1,41 +0,0 @@
// This CFC offers no protection--thus, anyone can access this.
// You can implement this by renaming the CFC with a UUID--that can secure
// this through obscurity. But, I prefer the approach shown in the ColdBox
// example. That is what I use. I have not tried this code :(
component
{
/** Loads the Web UI (HTML) **/
remote function index () returnformat='plain' {
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
return local.HothReport.getReportView();
}
/** Access Hoth report data as JSON.
@exception If not provided a list of exceptions will be returned.
If provided, the value should be an exception hash which
modified the behavior to return information for only
that exception. **/
remote function report (string exception) returnformat='JSON' {
local.report = (structKeyExists(arguments, 'exception')
? arguments.exception
: 'all');
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
return local.HothReport.report(local.report);
}
/** Delete a report. **/
remote function delete (string exception)returnformat='JSON' {
if (!structKeyExists(arguments, 'exception'))
{
// We can delete all exceptions at once!
arguments.exception = 'all';
}
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
// Delete!
return local.HothReporter.delete(arguments.exception);
}
}
@@ -1,79 +0,0 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
Default configuration for Hoth.
*/
component
implements='Hoth.object.iHothConfig'
extends='Hoth.object.CoreConfig'
accessors=true {
// -------------------------------------------------------------------------
// BASIC HOTH SETTINGS (required)
// -------------------------------------------------------------------------
/** What is the name of your application? */
property name='applicationName' default='HothDefaultConfig';
/** How many seconds should we lock file operations?
For most operations this is exclusive to a unique exception. */
property name='timeToLock' default='1';
/** Where would you like Hoth to save exception data?
This folder should be empty when you start. */
property name='logPath' default='/Hoth/examples/Example_Logs';
/** Is the log file location relative to the webroot?
This folder should be empty when you start. */
property name='logPathIsRelative' default='true';
// -------------------------------------------------------------------------
// HOTH EMAIL SETTINGS (required)
// -------------------------------------------------------------------------
/** Would you like new exceptions to be emailed to you? */
property name='EmailNewExceptions' default='false';
/** What address(es) should receive these e-mails? */
property name='EmailNewExceptionsTo' default='you@yourdomain.com';
/** What address would you like these emails sent from? */
property name='EmailNewExceptionsFrom' default='hoth@yourdomain.com';
/** Would you like the raw JSON attached to the e-mail? */
property name='EmailNewExceptionsFile' default='false';
/** Would you like HTML emails which contain the exception? */
property name='EmailExceptionsAsHTML' default='false';
// -------------------------------------------------------------------------
// HOTH REPORT SETTINGS (required)
// -------------------------------------------------------------------------
/** How you access the Hoth reports is up to you. When the reports are
generated Hoth needs to know how to build links so you can navigate
the report and get information from the server.
You are responsible for deciding how you wish to access reports. Once
you have figured that out, please, write the full URL here.
**/
property
name='HothReportURL'
default='http://office/lib/Hoth/examples/ColdFusion/HothReportUI.cfc';
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
public function init()
{
super.init();
return this;
}
}
-1
View File
@@ -1 +0,0 @@
/*.hoth
-3
View File
@@ -1,3 +0,0 @@
This folder is populated by Hoth to keep track of all applications that
use this instance of Hoth so Hoth can find the log files for your
applications when generating reports.
@@ -1,98 +0,0 @@
/*
--------------------------------------------------------------------
I configure and load Hoth into the ColdBox cache
--------------------------------------------------------------------
Define settings in ColdBox.cfc for example:
interceptors = [
//Autowire
{
class="coldbox.system.interceptors.Autowire",
properties={}
},
//SES
{
class="coldbox.system.interceptors.SES",
properties={}
},
//HothTracker
{
class="hoth.extras.ColdBoxInterceptor",
properties={
EmailNewExceptions = true,
EmailNewExceptionsFile = true,
EmailNewExceptionsTo = "kaboom@mysite.com",
EmailNewExceptionsFrom = "server@mysite.com"
}
}
];
--------------------------------------------------------------------
*/
component
{
/* CONSTRUCTOR
------------------------------------------- */
/**
* This is the configuration method for the interceptor
*/
void function Configure()
{
// check for required settings
if ( !propertyExists( "EmailNewExceptionsTo" ) )
{
getPlugin( "logger" ).fatal( "hoth.extras.ColdBoxInterceptor.Configure", "The required 'EmailNewExceptionsTo' property has not been defined" );
throw(
type="hoth.extras.ColdBoxInterceptor"
, message="The required 'EmailNewExceptionsTo' property has not been defined"
);
}
// Use ColdBox settings if not specified
if ( !propertyExists( "applicationName" ) )
{
setProperty( "applicationName", getSetting( "AppName" ) & " (" & getSetting( "Environment" ) & ")" );
}
if ( !propertyExists( "logPath" ) )
{
setProperty( "logPath", getSetting( "ApplicationPath" ) & "logs/hoth" );
setProperty( "logPathIsRelative", false );
}
// Optional settings
if ( !propertyExists( "cacheKeyName" ) )
{
setProperty( "cacheKeyName", "hothtracker" );
}
}
/* INTERCEPTION POINTS
------------------------------------------- */
void function afterAspectsLoad( required any event, required struct interceptData )
{
var HothConfig = "";
var HothTracker = "";
var key = "";
var properties = getProperties();
HothConfig = new hoth.config.HothConfig();
for ( key in properties )
{
// use evaluate to dynamically called setters
if ( StructKeyExists( HothConfig, "set" & key ) )
{
evaluate( "HothConfig.set#key#( properties[ key ] )" );
}
}
HothTracker = new Hoth.HothTracker( HothConfig );
// store in ColdBox Cache
getColdboxOCM().set( getProperty( 'cacheKeyName' ), HothTracker, 0 );
getPlugin( "logger" ).info( "hoth.extras.ColdBoxInterceptor", "HothTracker configured and loaded in ColdBox cache using key '#getProperty( 'cacheKeyName' )#'" );
}
}
-4
View File
@@ -1,4 +0,0 @@
This is the default location for Hoth logs.
You should provide a better location in your Hoth Config object that
is unique to your application.
@@ -1,74 +0,0 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
Core configuration object for Hoth. This object provides all the
functionality for a Hoth Config Object. Individual implementations should
extend this object and provide their own values within the property
declaration.
// -------------------------------------------------------------------------
Modified : 12/13/2010 10:04:06 AM by Aaron Greenlee.
- Added support for global settings.
*/
component
accessors='true'
{
/** Construct a configuration object for Hoth. */
public function init ()
{
local.md = getMetadata(this);
local.n = arrayLen(local.md.properties);
for (local.i=1; local.i <= local.n; local.i++) {
local.fn = this['set' & local.md.properties[local.i].name];
local.fn(local.md.properties[local.i]['default']);
}
return this;
}
public void function setGlobalDatabasePath(required string path) {
variables.GlobalDatabasePath = arguments.path;
}
public string function getGlobalDatabasePath() {
return (structKeyExists(variables, 'GlobalDatabasePath'))
? variables.GlobalDatabasePath
: '/Hoth/db/';
}
/** Expands a path **/
public string function getLogPathExpanded ()
{
if (!structKeyExists(variables, 'logPathIsRelative')) {
variables.logPathIsRelative = false;
}
return variables.logPathIsRelative ? expandPath( getLogPath() ) : getLogPath();
}
/** Return a path for Hoth. */
public string function getPath (name)
{
switch (arguments.name) {
// Sub-Directories
case 'incidents' :
case 'exceptions' :
return getLogPathExpanded() & '/' & lcase(arguments.name);
break;
// Files
case 'exceptionIndex' :
case 'exceptionReport' :
case 'exceptionReportActivity' :
return getLogPathExpanded() & '/' & lcase(arguments.name) & '.log';
break;
}
}
}
@@ -1,118 +0,0 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 12/13/2010
Responsible for helping Hoth report for multiple applications.
*/
component
name = 'HothApplicationManager'
accessors = true
{
property name='applications' type='array';
// The filename that holds application knowledge.
VARIABLES.APP_LOCK_KEY = 'HothApplicationDBFileLock';
/** Constructor.
* @HothConfig Requires a HothConfig Object.
*/
public Hoth.object.HothApplicationManager function init(
required HothConfig
){
setApplications( loadApplicationsFromDisk(HothConfig) );
return this;
}
/** To allow for easy reporting, Hoth needs to be able to track new
applications (or really, new log paths). When a new instance of
HothTracker is created, this method will be called. **/
public boolean function learnApplication(
required HothConfig
){
// Prevent any other threads from saving our new application
lock
name=VARIABLES.APP_LOCK_KEY
timeout=arguments.HothConfig.getTimeToLock()
type="exclusive"
{
local.knownApplications =
loadApplicationsFromDisk(argumentCollection=arguments);
for(local.index in local.knownApplications)
{
// Confirm our keys exist
if (structKeyExists(local.index, 'logPath'))
{
// If we find the same logPath, we know about this
// application and need to exit.
if (local.index.logPath == arguments.HothConfig.getLogPath())
{
return false;
}
}
}
// We did not find out application path within the file.
// Create the struct and append it to our array.
local.applicationDataToSave =
{
'applicationName' = arguments.HothConfig.getApplicationName()
,'logPath' = arguments.HothConfig.getLogPath()
,'created' = now()
};
arrayAppend(
local.knownApplications
,local.applicationDataToSave
);
// Save the contents of the entire array, replacing the entire file
local.path = arguments.HothConfig.getGlobalDatabasePath();
fileWrite(
expandPath(arguments.HothConfig.getGlobalDatabasePath() & 'applications.hoth')
,serializeJSON(local.knownApplications)
,'UTF-8'
);
}
return true;
}
/** Loads applications already observed by Hoth from disk.
* This process should be locked by the calling method.
* @HothConfig Requires a HothConfig Object. */
private array function loadApplicationsFromDisk (
required HothConfig
){
local.path = arguments.HothConfig.getGlobalDatabasePath();
local.appfile = expandPath(arguments.HothConfig.getGlobalDatabasePath() & 'applications.hoth');
// Return an empty array if the appFile does not exist.
if (!fileExists( local.appfile ) )
{
return [];
}
local.applicationsFound = fileRead( local.appfile );
// Delete the database if the file is invalid.
if (!isJSON(local.applicationsFound))
{
fileDelete( local.appfile );
return [];
}
// Convert the file to our desired structure
local.parsedDB = deserializeJSON(local.applicationsFound);
return local.parsedDB;
}
}
@@ -1,30 +0,0 @@
component
{
/**
* Returns the number of seconds since UTC January 1, 1970, 00:00:00
* (Epoch time).
*
* @param DateTime Date/time object you want converted to
* Epoch time.(Required)
*
* @return Returns a numeric value.
* @author Rob Brooks-Bilson (rbils@amkor.com)
* @version 1, June 21, 2002
*/
function GetEpochTimeFromLocal() {
local.datetime = 0;
if ( arrayLen(arguments) == 0)
{
local.datetime = Now();
} else {
local.datetime = arguments[1];
}
return
DateDiff(
"s"
,DateConvert("utc2Local", "January 1 1970 00:00")
,datetime);
}
}
@@ -1,19 +0,0 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
Interface for a Hoth Config Object.
*/
interface {
public function init ();
public string function getLogPathExpanded();
public string function getPath (name);
}
@@ -1,457 +0,0 @@
<cfcomponent displayname="bugLogListener">
<!---
bugLogListener.cfc
This is the main point of entry into the bugLog API. This component is the one that
actually processes the bug reports, it adds them to the database and is responsible
for processing any defined rules.
Created: 2007 - Oscar Arevalo - http://www.oscararevalo.com
--->
<cfset variables.startedOn = 0>
<cfset variables.oDAOFactory = 0>
<cfset variables.oRuleProcessor = 0>
<cfset variables.oConfig = 0>
<cfset variables.msgLog = arrayNew(1)>
<cfset variables.maxLogSize = 10>
<cfset variables.instanceName = "">
<cfset variables.autoCreateDefault = true>
<cffunction name="init" access="public" returntype="bugLogListener" hint="This is the constructor">
<cfargument name="config" required="true" type="config">
<cfargument name="instanceName" type="string" required="true">
<cfscript>
var cacheTTL = 300; // timeout in minutes for cache entries
variables.oConfig = arguments.config; // global configuration
variables.instanceName = arguments.instanceName;
logMessage("Starting BugLogListener (#instanceName#) service...");
// load settings
variables.maxLogSize = arguments.config.getSetting("service.maxLogSize");
// load DAO Factory
variables.oDAOFactory = createObject("component","bugLog.components.DAOFactory").init( variables.oConfig );
// load the finder objects
variables.oAppFinder = createObject("component","bugLog.components.appFinder").init( variables.oDAOFactory.getDAO("application") );
variables.oHostFinder = createObject("component","bugLog.components.hostFinder").init( variables.oDAOFactory.getDAO("host") );
variables.oSeverityFinder = createObject("component","bugLog.components.severityFinder").init( variables.oDAOFactory.getDAO("severity") );
variables.oSourceFinder = createObject("component","bugLog.components.sourceFinder").init( variables.oDAOFactory.getDAO("source") );
variables.oUserFinder = createObject("component","bugLog.components.userFinder").init( variables.oDAOFactory.getDAO("user") );
// load the rule processor
variables.oRuleProcessor = createObject("component","bugLog.components.ruleProcessor").init();
// create cache instances
variables.oAppCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
variables.oHostCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
variables.oSeverityCache = createObject("component","bugLog.components.lib.cache.cacheService").init(10, cacheTTL, false);
variables.oSourceCache = createObject("component","bugLog.components.lib.cache.cacheService").init(5, cacheTTL, false);
variables.oUserCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
// load scheduler
variables.scheduler = createObject("component","bugLog.components.schedulerService").init( variables.oConfig, variables.instanceName );
// load the mailer service
variables.mailerService = createObject("component","bugLog.components.MailerService").init( variables.oConfig );
// load rules
loadRules();
// configure history purging
configureHistoryPurging();
// configure the digest sender
configureDigest();
// record the date at which the service started
variables.startedOn = Now();
logMessage("BugLogListener Service (#instanceName#) Started");
</cfscript>
<cfreturn this>
</cffunction>
<cffunction name="logEntry" access="public" returntype="void" hint="This method adds a bug report entry into BugLog. Bug reports must be passed as RawEntryBeans">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfscript>
var bean = arguments.entryBean;
var oEntry = 0;
var oApp = 0;
var oHost = 0;
var oSeverity = 0;
var oSource = 0;
var oDF = variables.oDAOFactory;
// get autocreate settings
var autoCreateApp = allowAutoCreate("application");
var autoCreateHost = allowAutoCreate("host");
var autoCreateSeverity = allowAutoCreate("severity");
var autoCreateSource = allowAutoCreate("source");
// extract related objects from bean
oApp = getApplicationFromBean( bean, autoCreateApp );
oHost = getHostFromBean( bean, autoCreateHost );
oSeverity = getSeverityFromBean( bean, autoCreateSeverity );
oSource = getSourceFromBean( bean, autoCreateSource );
// create entry
oEntry = createObject("component","bugLog.components.entry").init( oDF.getDAO("entry") );
oEntry.setDateTime(bean.getdateTime());
oEntry.setMessage(bean.getmessage());
oEntry.setApplicationID(oApp.getApplicationID());
oEntry.setSourceID(oSource.getSourceID());
oEntry.setSeverityID(oSeverity.getSeverityID());
oEntry.setHostID(oHost.getHostID());
oEntry.setExceptionMessage(bean.getexceptionMessage());
oEntry.setExceptionDetails(bean.getexceptionDetails());
oEntry.setCFID(bean.getcfid());
oEntry.setCFTOKEN(bean.getcftoken());
oEntry.setUserAgent(bean.getuserAgent());
oEntry.setTemplatePath(bean.gettemplate_Path());
oEntry.setHTMLReport(bean.getHTMLReport());
oEntry.setCreatedOn(bean.getReceivedOn());
// save entry
oEntry.save();
// process rules
variables.oRuleProcessor.processRules(bean, oEntry);
</cfscript>
</cffunction>
<cffunction name="getStartedOn" access="public" returntype="date" hint="Returns the date and time where this instance of BugLogListener was created">
<cfreturn variables.startedOn>
</cffunction>
<cffunction name="shutDown" access="public" returntype="void" hint="Performs any clean up action required">
<cfset logMessage("BugLogListener service stopped.")>
</cffunction>
<cffunction name="logMessage" access="public" output="false" returntype="void" hint="this method appends an entry to the messages log as well as displays the message on the console">
<cfargument name="msg" type="string" required="true" />
<cfset var System = CreateObject('java','java.lang.System') />
<cfset var txt = timeFormat(now(), 'HH:mm:ss') & ": " & msg>
<cfset System.out.println("BugLogListener: " & txt) />
<cflock name="bugLogListener_logMessage" type="exclusive" timeout="10">
<cfif arrayLen(variables.msgLog) gt variables.maxLogSize>
<cfset arrayDeleteAt(variables.msgLog, ArrayLen(variables.msgLog))>
</cfif>
<cfset arrayPrepend(variables.msgLog,txt)>
</cflock>
</cffunction>
<cffunction name="getConfig" returnType="any" access="public" hint="returns the config settings">
<cfreturn variables.oConfig>
</cffunction>
<cffunction name="getInstanceName" returnType="any" access="public" hint="returns the current instance name">
<cfreturn variables.instanceName>
</cffunction>
<cffunction name="validate" returntype="boolean" access="public" hint="Validates that a bug report is valid, if not throws an error. This only applies when the requireAPIKey setting is true, otherwise returns True always">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="apiKey" type="string" required="true">
<cfscript>
// validate API
if(getConfig().getSetting("service.requireAPIKey",false)) {
if(arguments.apiKey eq "") {
throw(message="Invalid API Key",type="bugLog.invalidAPIKey");
}
var masterKey = getConfig().getSetting("service.APIKey");
if(arguments.apiKey neq masterKey) {
var user = getUserByAPIKey(arguments.apiKey);
if(!user.getIsAdmin() and arrayLen(user.getAllowedApplications())) {
// key is good, but since the user is a non-admin
// we need to validate the user can post bugs to the requested
// application.
var app = getApplicationFromBean( entryBean, false );
if(!user.isApplicationAllowed(app)) {
throw(message="Application not allowed",type="applicationNotAllowed");
}
}
}
}
// validate application
if(!allowAutoCreate("application")) {
getApplicationFromBean( entryBean, false );
}
// validate host
if(!allowAutoCreate("host")) {
getHostFromBean( entryBean, false );
}
// validte severity
if(!allowAutoCreate("severity")) {
getSeverityFromBean( entryBean, false );
}
// validate source
if(!allowAutoCreate("source")) {
getSourceFromBean( entryBean, false );
}
return true;
</cfscript>
</cffunction>
<cffunction name="reloadRules" access="public" returntype="void" hint="Reloads all rules">
<cfset loadRules()>
</cffunction>
<cffunction name="getMessageLog" access="public" returntype="array">
<cfreturn variables.msgLog>
</cffunction>
<!---- Private Methods ---->
<cffunction name="getApplicationFromBean" access="private" returntype="app" hint="Uses the information on the rawEntryBean to retrieve the corresponding Application object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oApp = 0;
var oDF = variables.oDAOFactory;
key = bean.getApplicationCode();
try {
// first we try to get it from the cache
oApp = variables.oAppCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oApp = variables.oAppFinder.findByCode( key );
} catch(appFinderException.ApplicationCodeNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Application",type="invalidApplication");
oApp = createObject("component","bugLog.components.app").init( oDF.getDAO("application") );
oApp.setCode( key );
oApp.setName( key );
oApp.save();
}
// store entry in cache
variables.oAppCache.store( key, oApp );
}
</cfscript>
<cfreturn oApp>
</cffunction>
<cffunction name="getHostFromBean" access="private" returntype="host" hint="Uses the information on the rawEntryBean to retrieve the corresponding Host object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oHost = 0;
var oDF = variables.oDAOFactory;
key = bean.getHostName();
try {
// first we try to get it from the cache
oHost = variables.oHostCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oHost = variables.oHostFinder.findByName( key );
} catch(hostFinderException.HostNameNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Host",type="invalidHost");
oHost = createObject("component","bugLog.components.host").init( oDF.getDAO("host") );
oHost.setHostName(key);
oHost.save();
}
// store entry in cache
variables.oHostCache.store( key, oHost );
}
</cfscript>
<cfreturn oHost>
</cffunction>
<cffunction name="getSeverityFromBean" access="private" returntype="severity" hint="Uses the information on the rawEntryBean to retrieve the corresponding Severity object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oSeverity = 0;
var oDF = variables.oDAOFactory;
key = bean.getSeverityCode();
try {
// first we try to get it from the cache
oSeverity = variables.oSeverityCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oSeverity = variables.oSeverityFinder.findByCode( key );
} catch(severityFinderException.codeNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Severity",type="invalidSeverity");
oSeverity = createObject("component","bugLog.components.severity").init( oDF.getDAO("severity") );
oSeverity.setCode( key );
oSeverity.setName( key );
oSeverity.save();
}
// store entry in cache
variables.oSeverityCache.store( key, oSeverity );
}
</cfscript>
<cfreturn oSeverity>
</cffunction>
<cffunction name="getUserByAPIKey" access="private" returntype="user" hint="Finds a user object using by its API Key">
<cfargument name="apiKey" type="string" required="true">
<cfscript>
var oUser = 0;
try {
// first we try to get it from the cache
oUser = variables.oUserCache.retrieve( apiKey );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oUser = variables.oUserFinder.findByAPIKey( apiKey );
var qryUserApps = oDAOFactory.getDAO("userApplication").search(userID = oUser.getUserID());
var apps = oAppFinder.findByIDList(valueList(qryUserApps.applicationID));
oUser.setAllowedApplications(apps);
} catch(sourceFinderException.usernameNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
throw(message="Invalid API Key",type="bugLog.invalidAPIKey");
}
// store entry in cache
variables.oUserCache.store( key, oUser );
}
return oUser;
</cfscript>
</cffunction>
<cffunction name="getSourceFromBean" access="private" returntype="source" hint="Uses the information on the rawEntryBean to retrieve the corresponding Source object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oSource = 0;
var oDF = variables.oDAOFactory;
key = bean.getSourceName();
try {
// first we try to get it from the cache
oSource = variables.oSourceCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oSource = variables.oSourceFinder.findByName( key );
} catch(sourceFinderException.codeNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Source",type="invalidSource");
oSource = createObject("component","bugLog.components.source").init( oDF.getDAO("source") );
oSource.setName( key );
oSource.save();
}
// store entry in cache
variables.oSourceCache.store( key, oSource );
}
</cfscript>
<cfreturn oSource>
</cffunction>
<cffunction name="loadRules" access="private" returntype="void" hint="this method loads the rules into the rule processor">
<cfscript>
var oRule = 0;
var oExtensionsService = 0;
var aRules = arrayNew(1);
var i = 0;
var dao = 0;
var thisRule = 0;
// clear all existing rules
variables.oRuleProcessor.flushRules();
// get the rule definitions from the extensions service
dao = variables.oDAOFactory.getDAO("extension");
oExtensionsService = createObject("component","bugLog.components.extensionsService").init( dao );
aRules = oExtensionsService.getRules();
// create rule objects and load them into the rule processor
for(i=1; i lte arrayLen(aRules);i=i+1) {
thisRule = aRules[i];
if(thisRule.enabled) {
oRule =
thisRule.instance
.setListener(this)
.setDAOFactory( variables.oDAOFactory )
.setMailerService( variables.mailerService );
// add rule to processor
variables.oRuleProcessor.addRule(oRule);
}
}
</cfscript>
</cffunction>
<cffunction name="configureHistoryPurging" access="private" output="false" returntype="void">
<cfset var enabled = getConfig().getSetting("purging.enabled")>
<cfif enabled>
<cfset scheduler.setupTask("bugLogPurgeHistory",
"util/purgeHistory.cfm",
"03:00",
"daily") />
<cfelse>
<cfset scheduler.removeTask("bugLogPurgeHistory") />
</cfif>
</cffunction>
<cffunction name="configureDigest" access="private" output="false" returntype="void">
<cfset var config = getConfig()>
<cfset var enabled = config.getSetting("digest.enabled")>
<cfset var interval = config.getSetting("digest.schedulerIntervalHours")>
<cfset var startTime = config.getSetting("digest.schedulerStartTime")>
<cfif enabled>
<cfset scheduler.setupTask("bugLogSendDigest",
"util/sendDigest.cfm",
startTime,
interval*3600) />
<cfelse>
<cfset scheduler.removeTask("bugLogSendDigest") />
</cfif>
</cffunction>
<cffunction name="allowAutoCreate" returnType="boolean" access="private">
<cfargument name="entityType" type="string" required="true">
<cfreturn getConfig().getSetting("autocreate." & arguments.entityType, variables.autoCreateDefault)>
</cffunction>
</cfcomponent>
-874
View File
@@ -1,874 +0,0 @@
component {
variables._fw1_version = "3.5.0";
variables._di1_version = "1.1.1";
/*
Copyright (c) 2010-2015, Sean Corfield
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.
*/
// CONSTRUCTOR
public any function init( any folders, struct config = { } ) {
variables.folderList = folders;
variables.folderArray = folders;
if ( isSimpleValue( folders ) ) {
variables.folderArray = listToArray( folders );
} else {
variables.folderList = arrayToList( folders );
}
var n = arrayLen( variables.folderArray );
for ( var i = 1; i <= n; ++i ) {
variables.folderArray[ i ] = trim( variables.folderArray[ i ] );
}
variables.config = config;
variables.beanInfo = { };
variables.beanCache = { };
variables.resolutionCache = { };
variables.initMethodCache = { };
variables.settersInfo = { };
variables.autoExclude = [
'/WEB-INF', '/Application.cfc', // never manage these!
'/Application.lc', '/Application.lucee',
// assume default name for intermediary:
'/MyApplication.cfc',
'/MyApplication.lc', '/MyApplication.lucee',
'framework.cfc', 'ioc.cfc', // legacy FW/1 / DI/1
// recent FW/1 + DI/1 + AOP/1 exclusions:
'/framework/aop.cfc', '/framework/beanProxy.cfc',
'/framework/ioc.cfc', '/framework/WireBoxAdapter.cfc',
'/framework/one.cfc',
// and Clojure-related exclusions:
'/framework/cfmljure.cfc', '/framework/cljcontroller.cfc',
'/framework/ioclj.cfc'
];
variables.listeners = 0;
setupFrameworkDefaults();
if ( structKeyExists( variables.config, 'loadListener' ) ) {
this.onLoad( variables.config.loadListener );
}
return this;
}
// PUBLIC METHODS
// programmatically register an alias
public any function addAlias( string aliasName, string beanName ) {
discoverBeans();
variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ];
return this;
}
// programmatically register new beans with the factory (add a singleton name/value pair)
public any function addBean( string beanName, any beanValue ) {
discoverBeans();
variables.beanInfo[ beanName ] = {
name = beanName, value = beanValue, isSingleton = true
};
return this;
}
// return true if the factory (or a parent factory) knows about the requested bean
public boolean function containsBean( string beanName ) {
discoverBeans();
return structKeyExists( variables.beanInfo, beanName ) ||
( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) );
}
// programmatically register new beans with the factory (add an actual CFC)
public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true, struct overrides = { } ) {
discoverBeans();
var singleDir = '';
if ( listLen( dottedPath, '.' ) > 1 ) {
var cfc = listLast( dottedPath, '.' );
var dottedPart = left( dottedPath, len( dottedPath ) - len( cfc ) - 1 );
singleDir = singular( listLast( dottedPart, '.' ) );
}
var basePath = replace( dottedPath, '.', '/', 'all' );
var cfcPath = expandPath( '/' & basePath & '.cfc' );
var expPath = cfcPath;
if ( !fileExists( expPath ) ) expPath = expandPath( '/' & basePath & '.lc' );
if ( !fileExists( expPath ) ) expPath = expandPath( '/' & basePath & '.lucee' );
if ( !fileExists( expPath ) ) throw "Unable to find source file for #dottedPath#: expands to #cfcPath#";
var cfcPath = replace( expPath, chr(92), '/', 'all' );
var metadata = {
name = beanName, qualifier = singleDir, isSingleton = isSingleton,
path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ),
overrides = overrides
};
variables.beanInfo[ beanName ] = metadata;
return this;
}
public any function factoryBean( string beanName, any factory, string methodName, array args = [ ], struct overrides = { } ) {
discoverBeans();
var metadata = {
name = beanName, isSingleton = false, // really?
factory = factory, method = methodName, args = args,
overrides = overrides
};
variables.beanInfo[ beanName ] = metadata;
return this;
}
// return the requested bean, fully populated
public any function getBean( string beanName ) {
discoverBeans();
if ( structKeyExists( variables.beanInfo, beanName ) ) {
return resolveBean( beanName );
} else if ( structKeyExists( variables, 'parent' ) ) {
return variables.parent.getBean( beanName );
} else {
throw 'bean not found: #beanName#';
}
}
// convenience API for metaprogramming perhaps?
public any function getBeanInfo( string beanName = '', boolean flatten = false,
string regex = '' ) {
discoverBeans();
if ( len( beanName ) ) {
// ask about a specific bean:
if ( structKeyExists( variables.beanInfo, beanName ) ) {
return variables.beanInfo[ beanName ];
}
if ( structKeyExists( variables, 'parent' ) ) {
return parentBeanInfo( beanName );
}
throw 'bean not found: #beanName#';
} else {
var result = { beanInfo = { } };
if ( structKeyExists( variables, 'parent' ) ) {
if ( flatten || len( regex ) ) {
structAppend( result.beanInfo, parentBeanInfoList( flatten ).beanInfo );
structAppend( result.beanInfo, variables.beanInfo );
} else {
result.beanInfo = variables.beanInfo;
result.parent = parentBeanInfoList( flatten );
};
} else {
result.beanInfo = variables.beanInfo;
}
if ( len( regex ) ) {
var matched = { };
for ( var name in result.beanInfo ) {
if ( REFind( regex, name ) ) {
matched[ name ] = result.beanInfo[ name ];
}
}
result.beanInfo = matched;
}
return result;
}
}
// return a copy of the DI/1 configuration
public struct function getConfig() {
// note: we only make a shallow copy
return structCopy( variables.config );
}
// return the DI/1 version
public string function getVersion() {
return variables.config.version;
}
// return true iff bean is known to be a singleton
public boolean function isSingleton( string beanName ) {
discoverBeans();
if ( structKeyExists( variables.beanInfo, beanName ) ) {
return variables.beanInfo[ beanName ].isSingleton;
} else if ( structKeyExists( variables, 'parent' ) ) {
try {
return variables.parent.isSingleton( beanName );
} catch ( any e ) {
return false; // parent doesn't know the bean therefore is it not singleton
}
} else {
return false; // we don't know the bean therefore it is not a managed singleton
}
}
// given a bean (by name, by type or by value), call the named
// setters with the specified property values
public any function injectProperties( any bean, struct properties ) {
if ( isSimpleValue( bean ) ) {
if ( containsBean( bean ) ) bean = getBean( bean );
else bean = construct( bean );
}
for ( var property in properties ) {
if ( !isNull( properties[ property ] ) ) {
var args = { };
args[ property ] = properties[ property ];
evaluate( 'bean.set#property#( argumentCollection = args )' );
}
}
return bean;
}
// empty the cache and reload all the singleton beans
// note: this does not reload the parent - if you have parent/child factories you
// are responsible for dealing with that logic (it's safe to reload a child but
// if you reload the parent, you must reload *all* child factories to ensure
// things stay consistent!)
public any function load() {
discoverBeans();
variables.beanCache = { };
variables.resolutionCache = { };
variables.initMethodCache = { };
for ( var key in variables.beanInfo ) {
if ( !structKeyExists( variables.beanInfo[ key ], "isSingleton" ) )
throw "internal error: bean #key# has no isSingleton flag!";
if ( variables.beanInfo[ key ].isSingleton ) {
getBean( key );
}
}
return this;
}
// add a listener for processing after a (re)load of the factory
// called with just the factory, should be a plain function
public any function onLoad( any listener ) {
var head = { next = variables.listeners, listener = listener };
variables.listeners = head;
return this;
}
// set the parent bean factory
public any function setParent( any parent ) {
variables.parent = parent;
return this;
}
// PRIVATE METHODS
private boolean function beanIsTransient( string singleDir, string dir, string beanName ) {
return singleDir == 'bean' ||
structKeyExists( variables.transients, dir ) ||
( structKeyExists( variables.config, "singletonPattern" ) &&
refindNoCase( variables.config.singletonPattern, beanName ) == 0 ) ||
( structKeyExists( variables.config, "transientPattern" ) &&
refindNoCase( variables.config.transientPattern, beanName ) > 0 );
}
private any function cachable( string beanName) {
var newObject = false;
var info = variables.beanInfo[ beanName ];
if ( info.isSingleton ) {
// cache on the qualified bean name:
var qualifiedName = beanName;
if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) {
qualifiedName = info.name & info.qualifier;
}
if ( !structKeyExists( variables.beanCache, qualifiedName ) ) {
variables.beanCache[ qualifiedName ] = construct( info.cfc );
newObject = true;
}
return { bean = variables.beanCache[ qualifiedName ], newObject = newObject };
} else {
return { bean = construct( info.cfc ), newObject = true };
}
}
private struct function cleanMetadata( string cfc ) {
var baseMetadata = metadata( cfc );
var iocMeta = { setters = { }, pruned = false, type = baseMetadata.type };
var md = { extends = baseMetadata };
do {
md = md.extends;
// gather up setters based on metadata:
var implicitSetters = false;
// we have implicit setters if: accessors="true" or persistent="true"
if ( structKeyExists( md, 'persistent' ) && isBoolean( md.persistent ) ) {
implicitSetters = md.persistent;
}
if ( structKeyExists( md, 'accessors' ) && isBoolean( md.accessors ) ) {
implicitSetters = implicitSetters || md.accessors;
}
if ( structKeyExists( md, 'properties' ) ) {
// due to a bug in ACF9.0.1, we cannot use var property in md.properties,
// instead we must use an explicit loop index... ugh!
var n = arrayLen( md.properties );
for ( var i = 1; i <= n; ++i ) {
var property = md.properties[ i ];
if ( implicitSetters &&
( !structKeyExists( property, 'setter' ) ||
isBoolean( property.setter ) && property.setter ) ) {
if ( structKeyExists( property, 'type' ) &&
property.type != 'any' ) {
iocMeta.setters[ property.name ] = 'typed';
} else {
iocMeta.setters[ property.name ] = 'implicit';
}
}
}
}
// still looking for a constructor?
if ( !structKeyExists( iocMeta, 'constructor' ) ) {
if ( structKeyExists( md, 'functions' ) ) {
// due to a bug in ACF9.0.1, we cannot use var property in md.functions,
// instead we must use an explicit loop index... ugh!
var n = arrayLen( md.functions );
for ( var i = 1; i <= n; ++i ) {
var func = md.functions[ i ];
if ( func.name == 'init' ) {
iocMeta.constructor = { };
if ( structKeyExists( func, 'parameters' ) ) {
// due to a bug in ACF9.0.1, we cannot use var arg in func.parameters,
// instead we must use an explicit loop index... ugh!
var m = arrayLen( func.parameters );
for ( var j = 1; j <= m; ++j ) {
var arg = func.parameters[ j ];
iocMeta.constructor[ arg.name ] = structKeyExists( arg, 'required' ) ? arg.required : false;
}
}
}
}
}
}
} while ( structKeyExists( md, 'extends' ) );
return iocMeta;
}
// in case an extension point wants to override actual construction:
private any function construct( string dottedPath ) {
return createObject( 'component', dottedPath );
}
// in case an extension point wants to override actual metadata retrieval:
private any function metadata( string dottedPath ) {
try {
return getComponentMetadata( dottedPath );
} catch ( any e ) {
var except = "Unable to getComponentMetadata(#dottedPath#) because: " &
e.message & ( len( e.detail ) ? " (#e.detail#)" : "" );
throw except;
}
}
private string function deduceDottedPath( string baseMapping, string basePath ) {
if ( right( basePath, 1 ) == '/' && len( basePath ) > 1 ) {
basePath = left( basePath, len( basePath ) - 1 );
}
var cfcPath = left( baseMapping, 1 ) == '/' ?
( len( baseMapping ) > 1 ? right( baseMapping, len( baseMapping ) - 1 ) : '' ) :
getFileFromPath( baseMapping );
if ( right( cfcPath, 1 ) == '/' && len( cfcPath ) > 1 ) {
cfcPath = left( cfcPath, len( cfcPath ) - 1 );
}
var expPath = basePath;
var notFound = true;
var dotted = '';
do {
var mapped = cfcPath;
if ( len( mapped ) && left( mapped, 1 ) != '.' ) mapped = '/' & mapped;
var mappedPath = replace( expandpath( mapped ), chr(92), '/', 'all' );
if ( mappedPath == basePath ) {
dotted = replace( cfcPath, '/', '.', 'all' );
notFound = false;
break;
}
var prevPath = expPath;
expPath = replace( getDirectoryFromPath( expPath ), chr(92), '/', 'all' );
if ( right( expPath, 1 ) == '/' && len( expPath ) > 1 ) {
expPath = left( expPath, len( expPath ) - 1 );
}
var progress = prevPath != expPath;
var piece = listLast( expPath, '/' );
cfcPath = piece & '/' & cfcPath;
} while ( progress );
if ( notFound ) {
throw 'unable to deduce dot-relative path for: #baseMapping# (#basePath#) root #expandPath("/")#';
}
return dotted;
}
private void function discoverBeans() {
if ( structKeyExists( variables, 'discoveryComplete' ) ) return;
lock name="#application.applicationName#_ioc1_#variables.folderList#" type="exclusive" timeout="30" {
if ( structKeyExists( variables, 'discoveryComplete' ) ) return;
variables.pathMapCache = { };
for ( var f in variables.folderArray ) {
discoverBeansInFolder( replace( f, chr(92), '/', 'all' ) );
}
variables.discoveryComplete = true;
}
onLoadEvent();
}
private void function discoverBeansInFolder( string mapping ) {
var folder = replace( expandPath( mapping ), chr(92), '/', 'all' );
var dotted = deduceDottedPath( mapping, folder );
var cfcs = [ ];
try {
cfcs = directoryList( folder, variables.config.recurse, 'path', '*.cfc' );
var lcs = directoryList( folder, variables.config.recurse, 'path', '*.lc' );
for ( var l in lcs ) arrayAppend( cfcs, l );
lcs = directoryList( folder, variables.config.recurse, 'path', '*.lucee' );
for ( l in lcs ) arrayAppend( cfcs, l );
} catch ( any e ) {
// assume bad path - ignore it, cfcs is empty list
}
for ( var cfcOSPath in cfcs ) {
var cfcPath = replace( cfcOSPath, chr(92), '/', 'all' );
// watch out for excluded paths:
var excludePath = false;
for ( var pattern in variables.config.exclude ) {
if ( findNoCase( pattern, cfcPath ) ) {
excludePath = true;
break;
}
}
if ( excludePath ) continue;
var relPath = right( cfcPath, len( cfcPath ) - len( folder ) );
var extN = 1 + len( listLast( cfcPath, "." ) );
relPath = left( relPath, len( relPath ) - extN );
var dir = listLast( getDirectoryFromPath( cfcPath ), '/' );
var singleDir = singular( dir );
var beanName = listLast( relPath, '/' );
var dottedPath = dotted & replace( relPath, '/', '.', 'all' );
try {
var metadata = {
name = beanName, qualifier = singleDir, isSingleton = !beanIsTransient( singleDir, dir, beanName ),
path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath )
};
if ( structKeyExists( metadata.metadata, "type" ) && metadata.metadata.type == "interface" ) {
continue;
}
if ( structKeyExists( variables.beanInfo, beanName ) ) {
if ( variables.config.omitDirectoryAliases ) {
throw '#beanName# is not unique (and omitDirectoryAliases is true)';
}
structDelete( variables.beanInfo, beanName );
variables.beanInfo[ beanName & singleDir ] = metadata;
} else {
variables.beanInfo[ beanName ] = metadata;
if ( !variables.config.omitDirectoryAliases ) {
variables.beanInfo[ beanName & singleDir ] = metadata;
}
}
} catch ( any e ) {
// wrap the exception so we can add bean name for debugging
// this trades off any stack trace information for the bean name but
// since we are only trying to get metadata, the latter should be
// more useful than the former
var except = "Problem with metadata for #beanName# (#dottedPath#) because: " &
e.message & ( len( e.detail ) ? " (#e.detail#)" : "" );
}
}
}
private struct function findSetters( any cfc, struct iocMeta ) {
var liveMeta = { setters = iocMeta.setters };
if ( !iocMeta.pruned ) {
// need to prune known setters of transients:
var prunable = { };
for ( var known in iocMeta.setters ) {
if ( !isSingleton( known ) ) {
prunable[ known ] = true;
}
}
for ( known in prunable ) {
structDelete( iocMeta.setters, known );
}
iocMeta.pruned = true;
}
// gather up explicit setters:
for ( var member in cfc ) {
var method = cfc[ member ];
var n = len( member );
if ( isCustomFunction( method ) && left( member, 3 ) == 'set' && n > 3 ) {
var property = right( member, n - 3 );
if ( !isSingleton( property ) ) {
// ignore properties that we know to be transients...
continue;
}
if ( !structKeyExists( liveMeta.setters, property ) ) {
liveMeta.setters[ property ] = 'explicit';
}
}
}
return liveMeta;
}
private any function forceCache( any bean, string beanName) {
var info = variables.beanInfo[ beanName ];
if ( info.isSingleton ) {
// cache on the qualified bean name:
var qualifiedName = beanName;
if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) {
qualifiedName = info.name & info.qualifier;
}
variables.beanCache[ qualifiedName ] = bean;
}
}
private boolean function isConstant ( string beanName ) {
return structKeyExists( variables.beanInfo, beanName ) &&
structKeyExists( variables.beanInfo[ beanName ], 'value' );
}
private void function logMissingBean( string beanName, string resolvingBeanName = '' ) {
var sys = createObject( 'java', 'java.lang.System' );
if ( len( resolvingBeanName ) ) {
sys.out.println( 'bean not found: #beanName#; while resolving #resolvingBeanName#' );
} else {
sys.out.println( 'bean not found: #beanName#' );
}
}
private void function missingBean( string beanName, string resolvingBeanName = '' ) {
if ( variables.config.strict ) {
if ( len( resolvingBeanName ) ) {
throw 'bean not found: #beanName#; while resolving #resolvingBeanName#';
} else {
throw 'bean not found: #beanName#';
}
} else {
logMissingBean( beanName, resolvingBeanName );
}
}
private void function onLoadEvent() {
var head = variables.listeners;
while ( isStruct( head ) ) {
if ( isCustomFunction( head.listener ) ||
( listFirst( server.coldfusion.productVersion ) >= 10 &&
isClosure( head.listener ) ) ) {
head.listener( this );
} else if ( isObject( head.listener ) ) {
head.listener.onLoad( this );
} else if ( isSimpleValue( head.listener ) &&
containsBean( head.listener ) ) {
getBean( head.listener ).onLoad( this );
} else {
throw "invalid onLoad listener registered: #head.listener.toString()#";
}
head = head.next;
}
}
private any function parentBeanInfo( string beanName ) {
// intended to be adaptable to whatever the parent is:
if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) {
// smells like DI/1 or compatible:
return variables.parent.getBeanInfo( beanName );
}
if ( structKeyExists( variables.parent, 'getBeanDefinition' ) ) {
// smells like ColdSpring or compatible:
return variables.parent.getBeanDefinition( beanName );
}
// unknown:
return { };
}
private any function parentBeanInfoList( boolean flatten ) {
// intended to be adaptable to whatever the parent is:
if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) {
// smells like DI/1 or compatible:
return variables.parent.getBeanInfo( flatten = flatten );
}
if ( structKeyExists( variables.parent, 'getBeanDefinitionList' ) ) {
// smells like ColdSpring or compatible:
return variables.parent.getBeanDefinitionList();
}
// unknown
return { };
}
private any function resolveBean( string beanName ) {
// do enough resolution to create and initialization this bean
// returns a struct of the bean and a struct of beans and setters still to run
// construction phase:
var partialBean = resolveBeanCreate( beanName, { injection = { }, dependencies = { } } );
if ( structKeyExists( variables.resolutionCache, beanName ) &&
variables.resolutionCache[ beanName ] ) {
// fully resolved, no action needed this time
} else {
var checkForPostInjection = structKeyExists( variables.config, 'initMethod' );
var initMethod = checkForPostInjection ? variables.config.initMethod : '';
var postInjectables = { };
// injection phase:
// now perform all of the injection:
for ( var name in partialBean.injection ) {
var injection = partialBean.injection[ name ];
if ( checkForPostInjection && !isConstant( name ) && structKeyExists( injection.bean, initMethod ) ) {
postInjectables[ name ] = true;
}
for ( var property in injection.setters ) {
if ( injection.setters[ property ] == 'typed' &&
variables.config.omitTypedProperties ) {
// we do not inject typed properties!
continue;
}
var args = { };
if ( structKeyExists( injection.overrides, property ) ) {
args[ property ] = injection.overrides[ property ];
} else if ( structKeyExists( partialBean.injection, property ) ) {
args[ property ] = partialBean.injection[ property ].bean;
} else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( property ) ) {
args[ property ] = variables.parent.getBean( property );
} else {
missingBean( property, beanName );
continue;
}
evaluate( 'injection.bean.set#property#( argumentCollection = args )' );
}
}
// post-injection, pre-init-method phase:
for ( name in partialBean.injection ) {
injection = partialBean.injection[ name ];
setupInitMethod( name, injection.bean );
}
// see if anything needs post-injection, init-method calls:
for ( var postName in postInjectables ) {
callInitMethod( postName, postInjectables, partialBean, initMethod );
}
variables.resolutionCache[ beanName ] = isSingleton( beanName );
}
return partialBean.bean;
}
private void function callInitMethod( string name, struct injectables, struct info, string method ) {
if ( injectables[ name ] ) {
injectables[ name ] = false; // this ensures we don't try to init the same
// bean twice - and also breaks circular dependencies...
if ( structKeyExists( info.dependencies, name ) ) {
for ( var depName in info.dependencies[ name ] ) {
if ( structKeyExists( injectables, depName ) &&
injectables[ depName ] ) {
callInitMethod( depName, injectables, info, method );
}
}
}
if ( structKeyExists( variables.initMethodCache, name ) &&
variables.initMethodCache[ name ] ) {
} else {
variables.initMethodCache[ name ] = isSingleton( name );
var bean = info.injection[ name ].bean;
evaluate( 'bean.#method#()' );
}
}
}
private struct function resolveBeanCreate( string beanName, struct accumulator ) {
var bean = 0;
if ( structKeyExists( variables.beanInfo, beanName ) ) {
var info = variables.beanInfo[ beanName ];
accumulator.dependencies[ beanName ] = { };
if ( structKeyExists( info, 'cfc' ) ) {
/*******************************************************/
var metaBean = cachable( beanName );
var overrides = structKeyExists( info, 'overrides' ) ? info.overrides : { };
bean = metaBean.bean;
if ( metaBean.newObject ) {
if ( structKeyExists( info.metadata, 'constructor' ) ) {
var args = { };
for ( var arg in info.metadata.constructor ) {
accumulator.dependencies[ beanName ][ arg ] = true;
var argBean = { };
// handle known required arguments
if ( info.metadata.constructor[ arg ] ) {
var beanMissing = true;
if ( structKeyExists( overrides, arg ) ) {
args[ arg ] = overrides[ arg ];
beanMissing = false;
} else if ( containsBean( arg ) ) {
argBean = resolveBeanCreate( arg, accumulator );
if ( structKeyExists( argBean, 'bean' ) ) {
args[ arg ] = argBean.bean;
beanMissing = false;
}
}
if ( beanMissing ) {
throw 'bean not found: #arg#; while resolving constructor arguments for #beanName#';
}
} else {
if ( structKeyExists( overrides, arg ) ) {
args[ arg ] = overrides[ arg ];
} else if ( containsBean( arg ) ) {
// optional but present
argBean = resolveBeanCreate( arg, accumulator );
if ( structKeyExists( argBean, 'bean' ) ) {
args[ arg ] = argBean.bean;
}
} else {
// optional but not present
}
}
}
var __ioc_newBean = evaluate( 'bean.init( argumentCollection = args )' );
// if the constructor returns anything, it becomes the bean
// this allows for smart constructors that return things other
// than the CFC being created, such as implicit factory beans
// and automatic singletons etc (rare practices in CFML but...)
if ( !isNull( __ioc_newBean ) ) {
bean = __ioc_newBean;
forceCache( bean, beanName );
}
}
}
/*******************************************************/
if ( !structKeyExists( accumulator.injection, beanName ) ) {
if ( !structKeyExists( variables.settersInfo, beanName ) ) {
variables.settersInfo[ beanName ] = findSetters( bean, info.metadata );
}
var setterMeta = {
setters = variables.settersInfo[ beanName ].setters,
bean = bean,
overrides = overrides
};
accumulator.injection[ beanName ] = setterMeta;
for ( var property in setterMeta.setters ) {
accumulator.dependencies[ beanName ][ property ] = true;
if ( structKeyExists( overrides, property ) ) {
// skip resolution because we'll inject override
} else {
resolveBeanCreate( property, accumulator );
}
}
}
accumulator.bean = bean;
} else if ( isConstant( beanName ) ) {
accumulator.bean = info.value;
accumulator.injection[ beanName ] = { bean = info.value, setters = { } };
} else if ( structKeyExists( info, 'factory' ) ) {
var fmBean = isSimpleValue( info.factory ) ? this.getBean( info.factory ) : info.factory;
var nArgs = arrayLen( info.args );
var argStruct = { };
for ( var i = 1; i <= nArgs; ++i ) {
var argName = info.args[ i ];
if ( structKeyExists( info.overrides, argName ) ) {
argStruct[ i ] = info.overrides[ argName ];
} else {
argStruct[ i ] = this.getBean( argName );
}
}
accumulator.bean = evaluate( 'fmBean.#info.method#(argumentCollection=argStruct)' );
accumulator.injection[ beanName ] = { bean = accumulator.bean, setters = { } };
} else {
throw 'internal error: invalid metadata for #beanName#';
}
} else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ) {
bean = variables.parent.getBean( beanName );
accumulator.injection[ beanName ] = { bean = bean, setters = { } };
accumulator.bean = bean;
} else {
missingBean( beanName );
}
return accumulator;
}
private void function setupFrameworkDefaults() {
param name = "variables.config.recurse" default = true;
param name = "variables.config.strict" default = false;
if ( !structKeyExists( variables.config, 'exclude' ) ) {
variables.config.exclude = [ ];
}
for ( var elem in variables.autoExclude ) {
arrayAppend( variables.config.exclude, replace( elem, chr(92), '/', 'all' ) );
}
// install bean factory constant:
variables.beanInfo.beanFactory = { value = this, isSingleton = true };
if ( structKeyExists( variables.config, 'constants' ) ) {
for ( var beanName in variables.config.constants ) {
variables.beanInfo[ beanName ] = { value = variables.config.constants[ beanName ], isSingleton = true };
}
}
variables.transients = { };
if ( structKeyExists( variables.config, 'transients' ) ) {
for ( var transientFolder in variables.config.transients ) {
variables.transients[ transientFolder ] = true;
}
}
if ( structKeyExists( variables.config, 'singletonPattern' ) &&
structKeyExists( variables.config, 'transientPattern' ) ) {
throw 'singletonPattern and transientPattern are mutually exclusive';
}
if ( !structKeyExists( variables.config, 'omitTypedProperties' ) ) {
variables.config.omitTypedProperties = false;
}
if ( !structKeyExists( variables.config, 'omitDirectoryAliases' ) ) {
variables.config.omitDirectoryAliases = false;
}
if ( !structKeyExists( variables.config, 'singulars' ) ) {
variables.config.singulars = { };
}
if ( !structKeyExists( variables.config, 'liberal' ) ) {
variables.config.liberal = false;
}
variables.config.version = variables._di1_version;
}
// hook for extension points to process beans after they have been
// constructed and injected, but before init-method is called on anything
private void function setupInitMethod( string name, any bean ) {
}
private string function singular( string plural ) {
if ( structKeyExists( variables.config.singulars, plural ) ) {
return variables.config.singulars[ plural ];
}
var single = plural;
var n = len( plural );
var last = right( plural, 1 );
if ( last == 's' ) {
if ( variables.config.liberal && n > 3 && right( plural, 3 ) == 'ies' ) {
single = left( plural, n - 3 ) & 'y';
} else {
single = left( plural, n - 1 );
}
}
return single;
}
}
-9
View File
@@ -1,9 +0,0 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/basicauth">
<cffunction name="get">
<cfargument name="username" default="" />
<cfargument name="password" default="" />
<cfreturn representationOf(arguments) />
</cffunction>
</cfcomponent>
@@ -1,11 +0,0 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/conflict/{URI}, /conflict/{URI}" hint="I have multiple uris">
<cffunction name="get">
<cfargument name="uri" type="string" default="0" />
<cfset local.res = {} />
<cfset local.res.uri = arguments.uri />
<cfreturn representationOf(local.res) />
</cffunction>
</cfcomponent>
-27
View File
@@ -1,27 +0,0 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo/{id}" hint="I am the component hint.">
<cfproperty name="dependency1" />
<cffunction name="get" hint="I am the function hint.">
<cfargument name="id" hint="I am the argument hint." />
<cfset local.headers = {}/>
<cfset local.headers['x-dude'] = "dude!" />
<cfset local.res = {} />
<cfset local.res["id"] = arguments.id />
<cfif structKeyExists(arguments, "dataFromOTR")>
<cfset local.res.dataFromOTR = arguments.dataFromOTR />
</cfif>
<cfreturn representationOf(local.res).withStatus(999).withHeaders(local.headers) />
</cffunction>
<cffunction name="put">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>
<cffunction name="post">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>
</cfcomponent>
@@ -1,38 +0,0 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo/tunnel/{id}">
<cffunction name="get">
<cfargument name="id" />
<cfset var echo = {} />
<cfset echo.actualMethod = "get" />
<cfreturn representationOf(echo) />
</cffunction>
<cffunction name="put">
<cfargument name="id" />
<cfset var echo = {} />
<cfset echo.actualMethod = "put" />
<cfreturn representationOf(echo).withStatus(200) />
</cffunction>
<cffunction name="post">
<cfargument name="id" />
<cfset var echo = {} />
<cfset echo.actualMethod = "post" />
<cfreturn representationOf(echo).withStatus(201) />
</cffunction>
<cffunction name="delete">
<cfargument name="id" />
<cfset var echo = {} />
<cfset echo.actualMethod = "delete" />
<cfreturn representationOf(echo).withStatus(200) />
</cffunction>
</cfcomponent>
@@ -1,26 +0,0 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo_regex/{id:\d{5}}">
<cffunction name="get">
<cfargument name="id" />
<cfset local.headers = {}/>
<cfset local.headers['x-dude'] = "dude!" />
<cfreturn representationOf(arguments).withStatus(999).withHeaders(local.headers) />
</cffunction>
<cffunction name="put">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>
<cffunction name="post">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>
<cffunction name="setDependency2">
<cfargument name="dependency2" />
<cfset this.dependency2 = arguments.dependency2 />
</cffunction>
</cfcomponent>
-21
View File
@@ -1,21 +0,0 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo_alias/{id}, /echo_alias" hint="I have multiple uris">
<cffunction name="get">
<cfargument name="id" default="0" />
<cfset local.res = {} />
<cfset local.res.id = arguments.id />
<cfreturn representationOf(local.res) />
</cffunction>
<cffunction name="put">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>
<cffunction name="post">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>
</cfcomponent>
@@ -0,0 +1,14 @@
component extends="core.resource" taffy:uri="/users/{userId}/orders/{orderId}" {
/**
* GET /users/{userId}/orders/{orderId} - Returns order for a user
*/
function get(required string userId, required string orderId) {
return rep({
userId: arguments.userId,
orderId: arguments.orderId,
method: "GET"
});
}
}
@@ -0,0 +1,18 @@
component extends="core.resource" taffy:uri="/simple" {
/**
* GET /simple - Returns a simple response
*/
function get() {
return rep({ message: "Hello from simple resource", method: "GET" });
}
/**
* POST /simple - Creates something
*/
function post(string name = "") {
return rep({ message: "Created", name: arguments.name, method: "POST" })
.withStatus(201, "Created");
}
}

Some files were not shown because too many files have changed in this diff Show More