234 attempt to fix js error Uncaught SyntaxError: Unexpected identifier 'hidden'
This commit is contained in:
+1
-1
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Vendored
-14
@@ -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
@@ -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
@@ -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 }}"
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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&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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
}
|
||||||
|
|||||||
@@ -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>
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Vendored
-6
@@ -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);
|
|
||||||
-9
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+514
-218
@@ -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,' ');
|
};
|
||||||
} else {
|
for (var h in reqHeaders){
|
||||||
// syntax highlight json and then replace spaces at the start of each line (or after <br/>) with
|
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,' ')].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,' ');
|
||||||
|
} else {
|
||||||
|
// syntax highlight json and then replace spaces at the start of each line (or after <br/>) with
|
||||||
|
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,' ')].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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
+430
-537
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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}
|
|
||||||
Vendored
+1213
-29
File diff suppressed because it is too large
Load Diff
Vendored
-4
@@ -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});
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 — 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 — 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!-- docs/_coverpage.md -->
|
||||||
|
|
||||||
|
# Taffy <small>v4</small>
|
||||||
|
|
||||||
|
> The REST web service framework for ColdFusion & 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 ↗](https://github.com/atuttle/Taffy)
|
||||||
+924
-25
@@ -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 & 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
@@ -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 →</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 →](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 →</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -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
@@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name":"taffy-lucee5",
|
||||||
|
"web":{
|
||||||
|
"http":{
|
||||||
|
"port":8085
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app":{
|
||||||
|
"cfengine":"lucee@5"
|
||||||
|
},
|
||||||
|
"openBrowser":false
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name":"taffy-lucee6",
|
||||||
|
"web":{
|
||||||
|
"http":{
|
||||||
|
"port":8086
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app":{
|
||||||
|
"cfengine":"lucee@6"
|
||||||
|
},
|
||||||
|
"openBrowser":false
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name":"taffy-lucee7",
|
||||||
|
"web":{
|
||||||
|
"http":{
|
||||||
|
"port":8087
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app":{
|
||||||
|
"cfengine":"lucee@7"
|
||||||
|
},
|
||||||
|
"openBrowser":false
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"app":{
|
|
||||||
"cfengine":""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
/*.hoth
|
|
||||||
@@ -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' )#'" );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
Reference in New Issue
Block a user