<cfscript>

	// Step 1: Gather the raw data from somewhere (probably your database).
	users = [
		{ id: 1, name: "Sarah ""Stubs"" Smith", role: "Admin", joinedAt: createDate( 2020, 1, 13 ) },
		{ id: 2, name: "Tom Titto", role: "Manager", joinedAt: createDate( 2021, 3, 4 ) },
		{ id: 3, name: "Kit Caraway", role: "Manager", joinedAt: createDate( 2019, 10, 27 ) },
		{ id: 4, name: "Allan Allure, Jr.", role: "Designer", joinedAt: createDate( 2020, 8, 22 ) }
	];

	// Step 2: Prepare the raw data for encoding. This usually means adding a HEADER row
	// and encoding non-string values as strings (such as formatting dates).
	rows = [
		[
			"ID",
			"Name",
			"Role",
			"Joined At"
		]
	];

	for ( user in users ) {

		rows.append([
			user.id,
			user.name,
			user.role,
			user.joinedAt.dateFormat( "yyyy-mm-dd" )
		]);

	}

	// Step 3: Serialize the row data as a CSV (Comma Separated Value) payload.
	csvContent = encodeCsvData(
		rows = rows,
		rowDelimiter = chr( 10 ),
		fieldDelimiter = chr( 9 )
	);
	csvFilename = ( "users-" & now().dateFormat( "yyyy-mm-dd" ) & ".csv" );

	// Step 4: Serve-up that sweet, sweet encoded data!
	header
		name = "content-disposition"
		value = getContentDisposition( csvFilename )
	;
	// NOTE: By converting the CSV payload into a binary value and using the CFContent
	// tag, ColdFusion will add the content-length header for us. Furthermore, the
	// CFContent tag will also halt any subsequent processing of the request.
	content
		type = "text/csv; charset=utf-8"
		variable = charsetDecode( csvContent, "utf-8" )
	;

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I encoded the given two-dimensional array as a CSV (Comma Separated Value) payload.
	* Rows and fields are serialized as lists using the given delimiters. All fields are
	* wrapped in double-quotes.
	* 
	* CAUTION: All values are assumed to have ALREADY BEEN STRINGIFIED. As such, if you
	* want to have control over how things such as Dates are serialized, then that should
	* have already been done prior to calling this function.
	* 
	* @rows I am the two-dimensional collection being encoded.
	* @rowDelimiter I am the delimiter used to serialized the encoded rows.
	* @fieldDelimiter I am the delimiter used to serialize the encoded fields.
	*/
	private string function encodeCsvData(
		required array rows,
		required string rowDelimiter,
		required string fieldDelimiter
		) {

		var encodedData = rows
			.map(
				( row ) => {

					return( encodeCsvDataRow( row, fieldDelimiter ) );

				}
			)
			.toList( rowDelimiter )
		;

		return( encodedData );

	}


	/**
	* I encode the given collection of values for use in a CSV (Comma Separated Value)
	* row. The values are then serialized as a list using the given delimiter.
	* 
	* @row I am the collection being encoded.
	* @fieldDelimiter I am the list delimiter used in the encoding.
	*/
	private string function encodeCsvDataRow(
		required array row,
		required string fieldDelimiter
		) {

		var encodedRow = row
			.map( encodeCsvDataField )
			.toList( fieldDelimiter )
		;

		return( encodedRow );

	}


	/**
	* I encode the given value for use in a CSV (Comma Separated Value) field. The value
	* is wrapped in double-quotes and any embedded quotes are "escaped" using the
	* standard "doubling" syntax.
	* 
	* @value I am the field value being encoded.
	*/
	private string function encodeCsvDataField( required string value ) {

		var encodedValue = replace( value, """", """""", "all" );

		return( """#encodedValue#""" );

	}


	/**
	* I get the content-disposition (attachment) header value for the given filename.
	* 
	* @filename I am the name of the file to be saved on the client.
	*/
	private string function getContentDisposition( required string filename ) {

		var encodedFilename = encodeForUrl( filename );

		return( "attachment; filename=""#encodedFilename#""; filename*=UTF-8''#encodedFilename#" );

	}

</cfscript>