<cfcomponent
    displayname="ReST API helper"
    output="true"
    hint="Static helper methods for ReST API">
	<!--- v0.02 2024-10-15 --->
	<!--- v0.03 2024-11-27 --->
	
	
<cffunction name="empty2null" 
	access="public" 
	returntype="any" 
	output="false"
	hint="replace empty field with null">
	
	<cfargument name="value" type="any" required="true" />
	
	<cfif isEmpty(ARGUMENTS.value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
		<cfreturn javacast('null','')/>
	<cfelse>
		<cfreturn ARGUMENTS.value />
	</cfif>
</cffunction>


<cffunction name="passThrough"	
	returntype="any" 
	output="false"
	hint="just return argument">	
	<cfargument name="x" type="ANY" required="true"/>
	<cfreturn #ARGUMENTS.x#/>	
</cffunction>
	

<cffunction name="appendRecord" 
	access="public" 
	returntype="any" 
	output="false"
	hint="put data into struct, replace empty fields with null">
	
	<cfargument name="struct" type="struct" required="true" />
	<cfargument name="key" type="string" required="true" />
	<cfargument name="fieldMap" type="struct" required="true" />
	<cfargument name="query" type="query" required="true" />
	<cfargument name="fieldNameDecorator" type="function" required="false" default=#passThrough#/>
	
	<cfif len(ARGUMENTS.key)>		
		<cfset ARGUMENTS.struct[ARGUMENTS.key]=structNew()/>
		<cfset var container=ARGUMENTS.struct[ARGUMENTS.key]/>
	<cfelse>
		<cfset var container=ARGUMENTS.struct/>
	</cfif>

	<cfloop list=#structKeyList(ARGUMENTS.fieldMap)# index="local.col">
		<cfset var obj=container/>
		<cfset var formatter = structFind(ARGUMENTS.fieldMap[local.col], "formatter")/>
		<cfif structKeyExists(ARGUMENTS.fieldMap[#local.col#],"container")>
			<cfset var subContainerName = structFind(ARGUMENTS.fieldMap[local.col], "container")/>
			
			<cfif len(subContainerName)>
				<cfif !structKeyExists(container, subContainerName)>
					<cfset structInsert(container, subContainerName, structNew())/>					
				</cfif>
				<cfset obj=container[subContainerName]/>
			</cfif>
		</cfif>
		
		<cfif queryColumnExists(ARGUMENTS.query, local.col)>
			<cfset obj[ARGUMENTS.fieldNameDecorator(local.col)]=empty2null(formatter(ARGUMENTS.query[local.col][ARGUMENTS.query.currentRow]))/>
		</cfif>
		<!---*** не уверен в надежности конструкции во всех реализациях CF, контекст query передастся ли--->
	</cfloop>	
	
	<cfreturn ARGUMENTS.struct/>
		
</cffunction>


<cffunction name="parseFilterParams"	
	returntype="struct" 
	output="true"
	hint="parse and collect filter params, for operators see filter_build">
<!---e.g. duration=NEQ:2--->
<!---*** add test for multiple values--->
	<cfargument name="params" type="struct" required="true" />
	
	<cfset var out=structNew()/>
	<cfloop collection=#params# index="local.item">

		<cfset var urlParamName=snake2camel(lCase(local.item))/>
		<cfif structKeyExists(URL,urlParamName)>
			<cfset var rawValue=URL[urlParamName]/>
			<cfset var operator="EQ"/>
			<cfset var value=#rawValue#/>
			<cfif listLen(rawValue,":") GT 1>
				<cfset var operator=listGetAt(rawValue,1,":")/>			
				<cfset var value=listGetAt(rawValue,2,":")/>			
			</cfif>
			<cfif !isValid(ARGUMENTS.params[local.item].type,value)>
				<cfthrow  type="invalidParamValue" message="Filter parameter #urlParamName# value is not a valid #ARGUMENTS.params[local.item].type#"/><!---не выводим значение, чтобы исключить XSS--->
			</cfif>
			
			<cfif structKeyExists(ARGUMENTS.params[local.item], "prefix") AND len(ARGUMENTS.params[local.item].prefix)>
				<cfset var fieldName="#ARGUMENTS.params[local.item].prefix#.#local.item#"/>
			<cfelse>
				<cfset var fieldName=#local.item#/>
			</cfif>
			<cfset var rec={field=#fieldName#, val=#value#, ftype=#ARGUMENTS.params[local.item].type#, compare=#operator#}/><!---*** add list--->
			<cfset structInsert(out, local.item, rec, true)/>
		</cfif>
		
	</cfloop>
	<cfreturn out/>
</cffunction>


<cffunction name="parseOrderBy"	
	returntype="struct" 
	output="false"
	hint="parse and collect sort order param"><!--- deprecated --->

	<cfargument name="params" type="struct" required="true" />
	<cfargument name="orderBy" type="string" required="true" />
	
	<cfset var out={}/>

	<cfloop list=#ARGUMENTS.orderBy# index="local.item">
		<cfif listLen(local.item,".") GT 1>
			<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
			<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
				<cfcase value="ASC"><cfset var asc=true/></cfcase>
				<cfcase value="DESC"><cfset var asc=false/></cfcase>
				<cfdefaultcase>
					<cfthrow  type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
				</cfdefaultcase>
			</cfswitch>
		<cfelse>
			<cfset var fld=camel2snake(local.item)/>
			<cfset var asc=true/>			
		</cfif>
		
		<cfif structKeyExists(ARGUMENTS.params, fld)>
			<cfset structInsert(out,"#ARGUMENTS.params[fld].prefix#.#fld#", {fld="#ARGUMENTS.params[fld].prefix#.#fld#",asc=#asc#}, true)/>
		<cfelse>
			<cfthrow  type="InvalidParamValue" message="Invalid orderBy field"/>
		</cfif>		
	</cfloop>
	<cfreturn out/>
</cffunction>

<cffunction name="parseNumericOrder"	
	returntype="array" 
	output="false"
	hint="parse and collect sort order param with numeric notation">

	<cfargument name="fieldSet" type="struct" required="true" />
	<cfargument name="orderBy" type="string" required="true" />
	
	<cfset var out=[]/>

	<cfloop list=#ARGUMENTS.orderBy# index="local.item">
		<cfif listLen(local.item,".") GT 1>
			<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
			<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
				<cfcase value="ASC"><cfset var asc=true/></cfcase>
				<cfcase value="DESC"><cfset var asc=false/></cfcase>
				<cfdefaultcase>
					<cfthrow  type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
				</cfdefaultcase>
			</cfswitch>
		<cfelse>
			<cfset var fld=camel2snake(local.item)/>
			<cfset var asc=true/>			
		</cfif>
		<cfif structKeyExists(ARGUMENTS.fieldSet, fld)>
			<cfset arrayAppend(out,{fld="#ARGUMENTS.fieldSet[fld].ordinal#",asc=#asc#})/>
		<cfelse>
			<cfthrow  type="InvalidParamValue" message="Invalid orderBy field"/>
		</cfif>		
	</cfloop>
	<cfreturn out/>
</cffunction>


<!---<cffunction name="snake2camel" 
	access="public" 
	returntype="any" 
	output="false"
	hint="convert snake style name to camel style name">
	
	<cfargument name="snake" type="string" required="true" />
	
	<cfset var snake=ARGUMENTS.snake/>
	<cfset var camel=""/>
	<cfset var pos=1/>
	<cfloop condition="true">
		<cfset next=reFind("(_[a-z])", snake, pos, false)/>
		<cfif NOT next GT 0>
			<cfbreak/>
		</cfif>			
		<cfset camel="#camel##mid(snake,pos,next-pos)##uCase(mid(snake,next+1,1))#"/>		
		<cfset pos=next+2/>
	</cfloop>
	<cfset camel="#camel##mid(snake,pos,len(snake))#"/>

	<cfreturn #camel#/>	
</cffunction>--->

<!---not used any more--->
<cffunction name="query4json" access="public" returntype="any" output="false"
	hint="Converts query to an array of structs. Key names are lowercase. Empty fields are treated as nulls">

	<cfargument name="Query" type="query" required="true" />
	
	<cfset var out=arrayNew(1)/>
	<cfset var i=0/>
	
	<cfloop query=ARGUMENTS.Query>
		<cfset i=i+1/>
		<cfset out[i]=structNew()/>
		<cfloop list=#ARGUMENTS.Query.ColumnList# index="local.col">
			<cfset var value=ARGUMENTS.Query[local.col][currentRow]/>
			<cfif isEmpty(value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
				<cfset out[i][local.col]=javacast('null','')/>
			<cfelse>
				<cfset out[i][local.col]=ARGUMENTS.Query[local.col][currentRow]/>
			</cfif>
		</cfloop>
	</cfloop>	
	
	<cfreturn(out)/>
</cffunction>


<cffunction name="snake2camel" 
	access="public" 
	returntype="any" 
	output="false"
	hint="convert snake style name to camel style name">	
	<cfargument name="snake" type="string" required="true" />
	<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>	
</cffunction>


<cffunction name="camel2snake" 
	access="public" 
	returntype="any" 
	output="false"
	hint="convert camel style name to snake style name">	
	<cfargument name="snake" type="string" required="true" />
	<cfreturn #reReplace(ARGUMENTS.snake,"([A-Z])","_\l\1","ALL")#/>	
</cffunction>	

<cffunction name="formatMessage"
	access="public" 
	returntype="any" 
	output="true"
	hint="formats Exception for display">		
	
	<cfargument name="message" type="string" required="true" />
	<cfargument name="title" type="string" required="false" />

	<cfset var title=structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.message# />	
	
	<cfreturn {type="about:blank", title="#title#", detail="#ARGUMENTS.message#"}/>
</cffunction>


<cffunction name="formatException" 
	access="public" 
	returntype="any" 
	output="true"
	hint="formats Exception for display">		
	
	<cfargument name="ex" type="struct" required="true" />
	<cfargument name="title" type="string" required="false" />
	
	<cfset var detail = listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,": " )/>
	<cfset var title = structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.ex.message# />	
	
	<cfreturn formatMessage(detail, title) />
</cffunction>	

<cffunction name="formatBadRequestError" 
	access="public" 
	returntype="any" 
	output="true"
	hint="formats Bad Request Exception">		
	
	<cfargument name="ex" type="struct" required="true" />
	
	<cfset var detail=listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,":" )/>
	
	<cfreturn {type="about:blank", title="Bad Request", detail="#detail#"}/>
</cffunction>	

<!---<cffunction name="wrapResultSet" 
	access="public" 
	returntype="any" 
	output="true"
	hint="format resultset as json">	
	
	<cfargument name="qRead" type="query" required="true" />
	<cfargument name="titleMap" type="struct" required="true" />
	<cfargument name="startrow" type="integer" required="false" default="1" />
	
	<cfset var resultSet=[]/>
	<cfloop query=#ARGUMENTS.qRead# startRow=#ARGUMENTS.startrow#>
		<cfset var rec={}/>	
		
		<cfset appendRecord(rec, "", ARGUMENTS.titleMap, ARGUMENTS.qRead, snake2camel)/>
		<cfset arrayAppend(resultSet, rec)/>
	</cfloop>
	
	<cfset var out={
	pageSize=
	}/>
	

<cfcontent type="application/json"/>
<cfoutput>{
	"pageSize":#pageSize#,
	"page":#page#,
	"orderBy":"#orderBy#",
	"size":"#arrayLen(resultSet)#",
	"total":"#qTotal.cnt#",
	"results":#serializeJson(resultSet)#, 
	"queryDurationMs":#queryDurationMs#, <cfset runDurationMs=getTickCount()-request.startTickCount/>
	"runDurationMs":#runDurationMs#
}</cfoutput>--->
	<cffunction name="validateField"
		access="public" 
		returntype="any" 
		output="false"
		hint="validate field of a structure, if exisits">	
		
		<cfargument name="struct" type="struct" required=true/>
		<cfargument name="name" required=true/>
		<cfargument name="type" default="string"/>
		<cfargument name="required" type="boolean" default="false"/>
		
		<cfif structKeyExists(ARGUMENTS.struct, ARGUMENTS.name)>
			<cfif NOT isValid(ARGUMENTS.type, ARGUMENTS.struct[ARGUMENTS.name])>
				<cfthrow type="invalidParamValue" message="Parameter validation failed" detail="Supplied value of '#ARGUMENTS.name#' is not a valid #ARGUMENTS.type#"/>
			</cfif>
		<cfelse>
			<cfif ARGUMENTS.required>
				<cfthrow type="invalidParamValue" message="Required field not supplied" detail="Required field #ARGUMENTS.name# not found"/>
			</cfif>
		</cfif>
	</cffunction>
	
	<!--- returns false if the key does not exist and throws exception on invalid format--->
	<cffunction name="keyExistsAndValid" access="public" returntype="boolean">
		<cfargument name="struct" type="struct" required=true/>
		<cfargument name="key" type="string" required=true/>
		<cfargument name="type" type="string" required=false/>
		<cfargument name="maxLength" type="numeric" required=false hint="Checked for strings only, negative value means no limit"/>
		
		<cfif structKeyExists(arguments.struct, arguments.key)>
			<cfif structKeyExists(arguments, "type")>
				<cfset var x=arguments.struct[arguments.key]/>
				<cfif isValid(arguments.type, x)>
					<cfif lCase(arguments.type) EQ "string">
						<cfif structKeyExists(arguments, "maxLength")>
							<cfif ARGUMENTS.maxLength GE 0>
								<cfif len(x) GT #arguments.maxLength#>
									<cfthrow type="invalidParamValue" message="Invalid parameter value (string too long)" detail="Parameter #arguments.key# length #len(x)# is greater than #arguments.maxLength#" errorcode=400/>
								</cfif>
							</cfif>
						</cfif>
					</cfif>					
				<cfelse>
					<cfthrow type="invalidParamValue" message="Invalid parameter value" detail="Parameter #arguments.key# is not a valid #arguments.type#" errorcode=400/>
				</cfif>
			</cfif>
			<cfreturn true/>
		<cfelse>
			<cfreturn false/>
		</cfif>
	</cffunction>
	
	<!--- returns false if the key does not exist or has invalid format--->
	<cffunction name="keyExistsAndValidGraceful" access="public" returntype="boolean">
		<cfargument name="struct" type="struct" required=true/>
		<cfargument name="key" type="string" required=true/>
		<cfargument name="type" type="string" required=false/>
		<cfargument name="maxLength" type="numeric" required=false default="-1" hint="Checked for strings only"/>		
		
		<cftry>
			<cfreturn keyExistsAndValid(ARGUMENTS.struct, ARGUMENTS.key, ARGUMENTS.type, ARGUMENTS.maxLength)/>
			<cfcatch type="ANY"><!---<cfrethrow/>---></cfcatch>
		</cftry>
		<cfreturn false/>
	</cffunction>
	
</cfcomponent>