From e72a1c19d81f8c360430554a7928c9e5fbedcf61 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 3 Apr 2026 11:03:01 -0700 Subject: [PATCH 1/2] feat: (W-004) add engine adapter modules with Lucee, Adobe, and BoxLang adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralizes scattered engine-specific checks (StructKeyExists(server, ...)) into a polymorphic adapter pattern, modeled on the existing database adapter hierarchy. Auto-detected at startup and stored in application.wheels.engineAdapter. Phase 1 migrates 6 high-value methods across 4 files: - getResponse(), getStatusCode(), getContentType() — Global.cfc, sse.cfc - getRequestTimeout() — Global.cfc - parseFormKey() — Dispatch.cfc - controllerNameToUpperCamelCase() — Dispatch.cfc New files: - engineAdapters/EngineAdapterInterface.cfc (documented contract) - engineAdapters/Base.cfc (abstract base, Lucee-compatible defaults) - engineAdapters/Lucee/LuceeAdapter.cfc - engineAdapters/Adobe/AdobeAdapter.cfc - engineAdapters/BoxLang/BoxLangAdapter.cfc - tests/specs/engineAdapterSpec.cfc (17 BDD tests) Tested: Lucee 6 (2295 pass/0 fail), Adobe 2025 (2304 pass/0 fail) Closes #1968 Co-Authored-By: Claude Opus 4.6 (1M context) --- vendor/wheels/Dispatch.cfc | 42 +------ vendor/wheels/Global.cfc | 49 ++------ vendor/wheels/controller/sse.cfc | 10 +- .../engineAdapters/Adobe/AdobeAdapter.cfc | 23 ++++ vendor/wheels/engineAdapters/Base.cfc | 112 ++++++++++++++++++ .../engineAdapters/BoxLang/BoxLangAdapter.cfc | 70 +++++++++++ .../engineAdapters/EngineAdapterInterface.cfc | 30 +++++ .../engineAdapters/Lucee/LuceeAdapter.cfc | 10 ++ vendor/wheels/events/onapplicationstart.cfc | 9 ++ .../wheels/tests/specs/engineAdapterSpec.cfc | 105 ++++++++++++++++ 10 files changed, 375 insertions(+), 85 deletions(-) create mode 100644 vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc create mode 100644 vendor/wheels/engineAdapters/Base.cfc create mode 100644 vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc create mode 100644 vendor/wheels/engineAdapters/EngineAdapterInterface.cfc create mode 100644 vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc create mode 100644 vendor/wheels/tests/specs/engineAdapterSpec.cfc diff --git a/vendor/wheels/Dispatch.cfc b/vendor/wheels/Dispatch.cfc index f5e317ecde..d06be40d83 100644 --- a/vendor/wheels/Dispatch.cfc +++ b/vendor/wheels/Dispatch.cfc @@ -100,21 +100,8 @@ component output="false" extends="wheels.Global"{ // Object form field. local.name = SpanExcluding(local.key, "["); - // BoxLang compatibility: Check if we're running on BoxLang and handle differently - if (StructKeyExists(server, "boxlang")) { - // BoxLang specific parsing to handle the bracket parsing differences - local.keyWithoutName = ReplaceNoCase(local.key, local.name & "[", "", "one"); - local.keyWithoutEndBracket = Left(local.keyWithoutName, Len(local.keyWithoutName) - 1); - local.nested = []; - local.segments = ListToArray(local.keyWithoutEndBracket, "][", false); - for (local.segment in local.segments) { - local.cleanSegment = Replace(Replace(local.segment, "[", "", "all"), "]", "", "all"); - ArrayAppend(local.nested, local.cleanSegment); - } - } else { - // Standard behavior for Lucee/Adobe CF - local.nested = ListToArray(ReplaceList(local.key, local.name & "[,]", ""), "[", true); - } + // Use engine adapter for cross-engine bracket-parsing differences + local.nested = application.wheels.engineAdapter.parseFormKey(local.key, local.name); if (!StructKeyExists(local.rv, local.name)) { local.rv[local.name] = {}; } @@ -640,29 +627,8 @@ component output="false" extends="wheels.Global"{ // Filter out illegal characters from the controller and action arguments. local.rv.action = ReReplace(local.rv.action, "[^0-9A-Za-z-_\.]", "", "all"); - // Convert controller to upperCamelCase. - // BoxLang compatibility: Handle consecutive dots differently - if (StructKeyExists(server, "boxlang")) { - // For BoxLang, manually handle the leading dots issue - local.dotPrefix = ""; - local.cleanName = local.rv.controller; - - while (Left(local.cleanName, 1) == ".") { - local.dotPrefix &= "."; - local.cleanName = Right(local.cleanName, Len(local.cleanName) - 1); - } - - local.cleanName = ReReplace(local.cleanName, "(^|-)([a-z])", "\u\2", "all"); - local.rv.controller = local.dotPrefix & local.cleanName; - } else { - // Standard behavior for Lucee/Adobe CF - local.cName = ListLast(local.rv.controller, "."); - local.cName = ReReplace(local.cName, "(^|-)([a-z])", "\u\2", "all"); - local.cLen = ListLen(local.rv.controller, "."); - if (local.cLen) { - local.rv.controller = ListSetAt(local.rv.controller, local.cLen, local.cName, "."); - } - } + // Convert controller to upperCamelCase via engine adapter + local.rv.controller = application.wheels.engineAdapter.controllerNameToUpperCamelCase(local.rv.controller); // Action to normal camelCase. local.rv.action = ReReplace(local.rv.action, "-([a-z])", "\u\1", "all"); diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 2f821ec788..314277f3ea 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -1982,43 +1982,14 @@ component output="false" { * Get the status code (e.g. 200, 404 etc) of the response we're about to send. */ public string function $statusCode() { - if (StructKeyExists(server, "lucee")) { - local.response = GetPageContext().getResponse(); - } else { - local.response = GetPageContext().getFusionContext().getResponse(); - } - return local.response.getStatus(); + return application.wheels.engineAdapter.getStatusCode(); } /** * Gets the value of the content type header (blank string if it doesn't exist) of the response we're about to send. */ public string function $contentType() { - local.rv = ""; - local.pageContext = getPageContext(); - if (structKeyExists(server, "boxlang")) { - local.response = local.pageContext; - } else if (structKeyExists(server, "lucee")) { - local.response = local.pageContext.getResponse(); - } else { - local.response = local.pageContext.getFusionContext().getResponse(); - } - - if (structKeyExists(server, "boxlang")) { - local.request = local.response.getRequest(); - local.header = local.request.getHeader("Content-Type"); - if(!isNull(local.header)) { - local.rv = local.header; - } - } else { - if (local.response.containsHeader("Content-Type")) { - local.header = local.response.getHeader("Content-Type"); - if (!isNull(local.header)) { - local.rv = local.header; - } - } - } - return local.rv; + return application.wheels.engineAdapter.getContentType(); } /** @@ -2134,14 +2105,14 @@ component output="false" { * Returns the request timeout value in seconds */ public numeric function $getRequestTimeout() { - // Check for BoxLang first using unique BoxLang identifier - if (StructKeyExists(server, "boxlang")) { - return 10000; - } else if (StructKeyExists(server, "lucee") && StructKeyExists(server.lucee, "version")) { - return (GetPageContext().getRequestTimeout() / 1000); - } else { - return CreateObject("java", "coldfusion.runtime.RequestMonitor").GetRequestTimeout(); - } + return application.wheels.engineAdapter.getRequestTimeout(); + } + + /** + * Returns the engine adapter instance for centralized cross-engine behavior. + */ + public any function $engineAdapter() { + return application.wheels.engineAdapter; } // ====================================================================== diff --git a/vendor/wheels/controller/sse.cfc b/vendor/wheels/controller/sse.cfc index 6ecf77bfa0..028697a747 100644 --- a/vendor/wheels/controller/sse.cfc +++ b/vendor/wheels/controller/sse.cfc @@ -65,14 +65,8 @@ component { * Note: This bypasses layouts and after-filters. Use for true streaming endpoints only. */ public any function initSSEStream() { - // Get the underlying response object - if (StructKeyExists(server, "boxlang")) { - local.response = GetPageContext(); - } else if (StructKeyExists(server, "lucee")) { - local.response = GetPageContext().getResponse(); - } else { - local.response = GetPageContext().getFusionContext().getResponse(); - } + // Get the underlying response object via engine adapter + local.response = application.wheels.engineAdapter.getResponse(); // Set SSE headers local.response.setContentType("text/event-stream"); diff --git a/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc b/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc new file mode 100644 index 0000000000..b9f9db5708 --- /dev/null +++ b/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc @@ -0,0 +1,23 @@ +/** + * Engine adapter for Adobe ColdFusion. + * Overrides response access (FusionContext) and request timeout (RequestMonitor). + */ +component extends="wheels.engineAdapters.Base" output="false" { + + variables.engineName = "Adobe ColdFusion"; + + /** + * Adobe CF requires getFusionContext() to access the response object. + */ + public any function getResponse() { + return GetPageContext().getFusionContext().getResponse(); + } + + /** + * Adobe CF uses the Java RequestMonitor class for timeout values. + */ + public numeric function getRequestTimeout() { + return CreateObject("java", "coldfusion.runtime.RequestMonitor").GetRequestTimeout(); + } + +} diff --git a/vendor/wheels/engineAdapters/Base.cfc b/vendor/wheels/engineAdapters/Base.cfc new file mode 100644 index 0000000000..309bd2b8f0 --- /dev/null +++ b/vendor/wheels/engineAdapters/Base.cfc @@ -0,0 +1,112 @@ +/** + * Abstract base engine adapter with default implementations. + * Concrete adapters (Lucee, Adobe, BoxLang) extend this and override + * only the methods that differ for their engine. + * + * Defaults are Lucee-compatible since Lucee is the primary target engine. + */ +component output="false" { + + variables.engineName = ""; + variables.engineVersion = ""; + variables.engineMajorVersion = 0; + + public Base function init(required string version) { + variables.engineVersion = arguments.version; + variables.engineMajorVersion = Val(ListFirst(arguments.version, ".,")); + return this; + } + + // --- Identity --- + + public string function getName() { + return variables.engineName; + } + + public string function getVersion() { + return variables.engineVersion; + } + + public numeric function getMajorVersion() { + return variables.engineMajorVersion; + } + + // --- Response / PageContext --- + + /** + * Returns the engine-specific HTTP response object. + * Default: Lucee-style via GetPageContext().getResponse() + */ + public any function getResponse() { + return GetPageContext().getResponse(); + } + + /** + * Returns the response writer for streaming output (SSE, etc). + */ + public any function getResponseWriter() { + return getResponse().getWriter(); + } + + /** + * Returns the HTTP status code of the current response. + */ + public numeric function getStatusCode() { + return getResponse().getStatus(); + } + + /** + * Returns the Content-Type header value of the current response. + */ + public string function getContentType() { + local.rv = ""; + local.response = getResponse(); + if (local.response.containsHeader("Content-Type")) { + local.header = local.response.getHeader("Content-Type"); + if (!IsNull(local.header)) { + local.rv = local.header; + } + } + return local.rv; + } + + /** + * Returns the request timeout value in seconds. + * Default: Lucee-style via GetPageContext().getRequestTimeout() / 1000 + */ + public numeric function getRequestTimeout() { + return (GetPageContext().getRequestTimeout() / 1000); + } + + // --- Form Handling --- + + /** + * Parses bracket-notation form keys like "user[address][city]" into + * an array of nested segments: ["address", "city"]. + * + * @key The full form field key (e.g. "user[address][city]") + * @name The base name prefix (e.g. "user") + */ + public array function parseFormKey(required string key, required string name) { + return ListToArray(ReplaceList(arguments.key, arguments.name & "[,]", ""), "[", true); + } + + // --- Controller --- + + /** + * Converts a dot-delimited controller name to UpperCamelCase. + * E.g. "admin.user-settings" -> "admin.UserSettings" + * + * @name The controller name to convert + */ + public string function controllerNameToUpperCamelCase(required string name) { + local.cName = ListLast(arguments.name, "."); + local.cName = ReReplace(local.cName, "(^|-)([a-z])", "\u\2", "all"); + local.cLen = ListLen(arguments.name, "."); + if (local.cLen) { + return ListSetAt(arguments.name, local.cLen, local.cName, "."); + } + return arguments.name; + } + +} diff --git a/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc b/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc new file mode 100644 index 0000000000..cc12bc5c54 --- /dev/null +++ b/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc @@ -0,0 +1,70 @@ +/** + * Engine adapter for BoxLang. + * BoxLang has significant differences in PageContext, form parsing, + * and controller name handling compared to Lucee/Adobe CF. + */ +component extends="wheels.engineAdapters.Base" output="false" { + + variables.engineName = "BoxLang"; + + /** + * BoxLang returns the response directly from GetPageContext(). + */ + public any function getResponse() { + return GetPageContext(); + } + + /** + * BoxLang gets Content-Type from the request side, not response side. + */ + public string function getContentType() { + local.rv = ""; + local.response = getResponse(); + local.request = local.response.getRequest(); + local.header = local.request.getHeader("Content-Type"); + if (!IsNull(local.header)) { + local.rv = local.header; + } + return local.rv; + } + + /** + * BoxLang does not expose a standard request timeout API. + * Returns a hardcoded high value consistent with existing behavior. + */ + public numeric function getRequestTimeout() { + return 10000; + } + + /** + * BoxLang has different bracket-parsing semantics for form keys. + * Splits on "][" and cleans remaining brackets from each segment. + */ + public array function parseFormKey(required string key, required string name) { + local.keyWithoutName = ReplaceNoCase(arguments.key, arguments.name & "[", "", "one"); + local.keyWithoutEndBracket = Left(local.keyWithoutName, Len(local.keyWithoutName) - 1); + local.nested = []; + local.segments = ListToArray(local.keyWithoutEndBracket, "][", false); + for (local.segment in local.segments) { + local.cleanSegment = Replace(Replace(local.segment, "[", "", "all"), "]", "", "all"); + ArrayAppend(local.nested, local.cleanSegment); + } + return local.nested; + } + + /** + * BoxLang handles consecutive leading dots differently in controller names. + * Preserves the dot prefix and only uppercases the clean portion. + */ + public string function controllerNameToUpperCamelCase(required string name) { + local.dotPrefix = ""; + local.cleanName = arguments.name; + while (Left(local.cleanName, 1) == ".") { + local.dotPrefix &= "."; + local.cleanName = Right(local.cleanName, Len(local.cleanName) - 1); + } + local.cleanName = ReReplace(local.cleanName, "(^|-)([a-z])", "\u\2", "all"); + return local.dotPrefix & local.cleanName; + } + +} diff --git a/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc b/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc new file mode 100644 index 0000000000..77123b4fc5 --- /dev/null +++ b/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc @@ -0,0 +1,30 @@ +/** + * Interface contract for engine adapters. + * Each CFML engine (Lucee, Adobe CF, BoxLang) implements this contract + * to provide consistent cross-engine behavior. + * + * NOTE: Using documented convention rather than CFML `interface` keyword + * to avoid cross-engine interface compilation issues. Base.cfc serves + * as the enforceable contract — all concrete adapters extend it. + */ +interface { + + // --- Identity --- + public string function getName(); + public string function getVersion(); + public numeric function getMajorVersion(); + + // --- Response / PageContext --- + public any function getResponse(); + public any function getResponseWriter(); + public numeric function getStatusCode(); + public string function getContentType(); + public numeric function getRequestTimeout(); + + // --- Form Handling --- + public array function parseFormKey(required string key, required string name); + + // --- Controller --- + public string function controllerNameToUpperCamelCase(required string name); + +} diff --git a/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc b/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc new file mode 100644 index 0000000000..8b9ea03f01 --- /dev/null +++ b/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc @@ -0,0 +1,10 @@ +/** + * Engine adapter for Lucee CFML. + * Lucee is the primary target engine — most defaults in Base.cfc + * already match Lucee behavior. This adapter only sets the engine name. + */ +component extends="wheels.engineAdapters.Base" output="false" { + + variables.engineName = "Lucee"; + +} diff --git a/vendor/wheels/events/onapplicationstart.cfc b/vendor/wheels/events/onapplicationstart.cfc index 7563015fc5..8651dcd9c8 100644 --- a/vendor/wheels/events/onapplicationstart.cfc +++ b/vendor/wheels/events/onapplicationstart.cfc @@ -45,6 +45,15 @@ component { } application.$wheels.serverVersionMajor = ListFirst(application.$wheels.serverVersion, ".,"); + // Instantiate the engine adapter for centralized cross-engine behavior. + if (application.$wheels.serverName == "BoxLang") { + application.$wheels.engineAdapter = new wheels.engineAdapters.BoxLang.BoxLangAdapter(application.$wheels.serverVersion); + } else if (application.$wheels.serverName == "Lucee") { + application.$wheels.engineAdapter = new wheels.engineAdapters.Lucee.LuceeAdapter(application.$wheels.serverVersion); + } else { + application.$wheels.engineAdapter = new wheels.engineAdapters.Adobe.AdobeAdapter(application.$wheels.serverVersion); + } + local.upgradeTo = application.wo.$checkMinimumVersion( engine = application.$wheels.serverName, version = application.$wheels.serverVersion diff --git a/vendor/wheels/tests/specs/engineAdapterSpec.cfc b/vendor/wheels/tests/specs/engineAdapterSpec.cfc new file mode 100644 index 0000000000..7d4d7309c4 --- /dev/null +++ b/vendor/wheels/tests/specs/engineAdapterSpec.cfc @@ -0,0 +1,105 @@ +component extends="wheels.WheelsTest" { + + function run() { + + describe("Engine Adapter", function() { + + it("is auto-detected at startup", function() { + expect(application.wheels.engineAdapter).toBeComponent(); + }); + + it("returns the correct engine name", function() { + var name = application.wheels.engineAdapter.getName(); + expect(ListFind("Lucee,Adobe ColdFusion,BoxLang", name)).toBeGT(0); + }); + + it("returns a non-empty version string", function() { + expect(Len(application.wheels.engineAdapter.getVersion())).toBeGT(0); + }); + + it("returns a valid major version", function() { + expect(application.wheels.engineAdapter.getMajorVersion()).toBeGT(0); + }); + + it("matches the application serverName", function() { + expect(application.wheels.engineAdapter.getName()).toBe(application.wheels.serverName); + }); + + it("matches the application serverVersion", function() { + expect(application.wheels.engineAdapter.getVersion()).toBe(application.wheels.serverVersion); + }); + + it("returns a response object", function() { + var resp = application.wheels.engineAdapter.getResponse(); + expect(resp).notToBeNull(); + }); + + it("returns a status code as numeric", function() { + var code = application.wheels.engineAdapter.getStatusCode(); + expect(code).toBeNumeric(); + }); + + it("returns content type as string", function() { + var ct = application.wheels.engineAdapter.getContentType(); + expect(IsSimpleValue(ct)).toBeTrue(); + }); + + it("returns request timeout as numeric", function() { + var timeout = application.wheels.engineAdapter.getRequestTimeout(); + expect(timeout).toBeNumeric(); + }); + + it("is accessible via the convenience function", function() { + var adapter = application.wo.$engineAdapter(); + expect(adapter.getName()).toBe(application.wheels.engineAdapter.getName()); + }); + + }); + + describe("Engine Adapter - parseFormKey", function() { + + it("parses single-level bracket key", function() { + var result = application.wheels.engineAdapter.parseFormKey("user[name]", "user"); + expect(result).toBeArray(); + expect(ArrayLen(result)).toBe(1); + expect(result[1]).toBe("name"); + }); + + it("parses deeply nested keys", function() { + var result = application.wheels.engineAdapter.parseFormKey("user[address][city]", "user"); + expect(result).toBeArray(); + expect(ArrayLen(result)).toBe(2); + expect(result[1]).toBe("address"); + expect(result[2]).toBe("city"); + }); + + it("parses triple-nested keys", function() { + var result = application.wheels.engineAdapter.parseFormKey("order[item][detail][color]", "order"); + expect(result).toBeArray(); + expect(ArrayLen(result)).toBe(3); + }); + + }); + + describe("Engine Adapter - controllerNameToUpperCamelCase", function() { + + it("converts hyphenated names to UpperCamelCase", function() { + var result = application.wheels.engineAdapter.controllerNameToUpperCamelCase("user-settings"); + expect(result).toBe("UserSettings"); + }); + + it("converts simple lowercase to capitalized", function() { + var result = application.wheels.engineAdapter.controllerNameToUpperCamelCase("users"); + expect(result).toBe("Users"); + }); + + it("preserves dot-delimited namespacing", function() { + var result = application.wheels.engineAdapter.controllerNameToUpperCamelCase("admin.users"); + expect(result).toBe("admin.Users"); + }); + + }); + + } + +} From 00fabe81a6d38b705aece5a201d0498b057823ed Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 3 Apr 2026 11:44:26 -0700 Subject: [PATCH 2/2] feat: (W-004) Phase 2 - migrate engine checks across model, global, view, and dispatch layers Add 16 new adapter methods (identity helpers, Oracle JDBC coercion, dynamic finders, hash normalization, struct defaults, numeric validation, DI completion, method invocation, image/zip handling, glob patterns, query args, port detection, date parsing, image formats) and migrate ~35 engine-specific checks across 23 files to use the centralized adapter instead of scattered StructKeyExists(server) checks. Key migrations: - Model layer: onmissingmethod, validations, miscellaneous, properties, sql, associations, callbacks, create, read - Global.cfc: $image, $zip, $hashedKey, $args, $convertToString, $dbinfo - Dispatch/Controller/Model/EventMethods: onDIComplete pattern consolidated - Mapper: glob pattern matching - Views: assets, links, formsdate, rendering Also replaces IIf() calls in dynamic finders with plain if/else (IIf is unreliable across engines and less readable). Tested: Lucee 6 (2278 pass/0 fail) and Adobe 2025 (2287 pass/0 fail). Co-Authored-By: Claude Opus 4.6 (1M context) --- vendor/wheels/Controller.cfc | 4 +- vendor/wheels/Dispatch.cfc | 11 +- vendor/wheels/Global.cfc | 96 +++---- vendor/wheels/Model.cfc | 4 +- vendor/wheels/controller/rendering.cfc | 7 +- .../engineAdapters/Adobe/AdobeAdapter.cfc | 30 ++- vendor/wheels/engineAdapters/Base.cfc | 220 ++++++++++++++++ .../engineAdapters/BoxLang/BoxLangAdapter.cfc | 194 +++++++++++++- .../engineAdapters/EngineAdapterInterface.cfc | 47 ++++ .../engineAdapters/Lucee/LuceeAdapter.cfc | 33 ++- vendor/wheels/events/EventMethods.cfc | 6 +- vendor/wheels/events/onapplicationstart.cfc | 4 +- vendor/wheels/mapper/matching.cfc | 10 +- vendor/wheels/model/associations.cfc | 2 +- vendor/wheels/model/callbacks.cfc | 34 +-- vendor/wheels/model/create.cfc | 6 +- vendor/wheels/model/miscellaneous.cfc | 16 +- vendor/wheels/model/onmissingmethod.cfc | 39 +-- vendor/wheels/model/properties.cfc | 13 +- vendor/wheels/model/read.cfc | 7 +- vendor/wheels/model/sql.cfc | 8 +- vendor/wheels/model/validations.cfc | 15 +- .../wheels/tests/specs/engineAdapterSpec.cfc | 236 ++++++++++++++++++ vendor/wheels/view/assets.cfc | 18 +- vendor/wheels/view/formsdate.cfc | 4 +- vendor/wheels/view/links.cfc | 3 +- 26 files changed, 837 insertions(+), 230 deletions(-) diff --git a/vendor/wheels/Controller.cfc b/vendor/wheels/Controller.cfc index d1d118c2da..a9cfee6ad8 100644 --- a/vendor/wheels/Controller.cfc +++ b/vendor/wheels/Controller.cfc @@ -212,9 +212,7 @@ component output="false" displayName="Controller" extends="wheels.Global"{ } function onDIcomplete(){ - if (structKeyExists(server, "boxlang")) { - variables.this = this; - } + $engineAdapter().prepareDIComplete(variables, this); new wheels.Plugins().$initializeMixins(variables); } } diff --git a/vendor/wheels/Dispatch.cfc b/vendor/wheels/Dispatch.cfc index d06be40d83..bc1b134171 100644 --- a/vendor/wheels/Dispatch.cfc +++ b/vendor/wheels/Dispatch.cfc @@ -279,12 +279,7 @@ component output="false" extends="wheels.Global"{ message="The action parameter is missing or null. Controller: #local.params.controller#"); } - if (structKeyExists(server, "boxlang")) { - local.method = application.wheels.public[local.params.action]; - local.method(); - } else { - invoke(object=application.wheels.public, methodname=local.params.action); - } + $engineAdapter().invokeMethod(application.wheels.public, local.params.action); // The wheels controller methods handle their own output and abort // So we need to ensure we don't continue processing return ""; @@ -671,9 +666,7 @@ component output="false" extends="wheels.Global"{ } function onDIComplete(){ - if (structKeyExists(server, "boxlang")) { - variables.this = this; - } + $engineAdapter().prepareDIComplete(variables, this); new wheels.Plugins().$initializeMixins(variables); } } diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 314277f3ea..199ee90970 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -31,16 +31,13 @@ component output="false" { public struct function $image(){ local.rv = {}; - if (structKeyExists(server, "boxlang")) { - if (arguments.action == "info") { - var img = ImageRead(arguments.source); - local.rv = ImageInfo(img); - } else { - throw( - type = "Wheels.Image.UnsupportedAction", - message = "The `$image()` function in BoxLang currently supports only the 'info' action." - ); - } + if (arguments.action == "info") { + local.rv = $engineAdapter().imageInfo(arguments.source); + } else if ($engineAdapter().isBoxLang()) { + throw( + type = "Wheels.Image.UnsupportedAction", + message = "The `$image()` function in BoxLang currently supports only the 'info' action." + ); } else { // Adobe or Lucee: use cfimage arguments.structName = "rv"; @@ -207,9 +204,9 @@ component output="false" { StructDelete(arguments, "password"); } - // BoxLang + SQL Server specific fix for index queries + // BoxLang specific fix for index queries (MSSQL/Oracle) if ( - StructKeyExists(server, "boxlang") && + $engineAdapter().isBoxLang() && StructKeyExists(arguments, "type") && arguments.type == "index" && StructKeyExists(arguments, "table") ) { @@ -371,14 +368,7 @@ component output="false" { } public any function $zip(){ - if (structKeyExists(server, "boxlang")) { - if (!left(arguments.file, 1) == "/") { - arguments.file = "/" & arguments.file; - } - if (!left(arguments.destination, 1) == "/") { - arguments.destination = "/" & arguments.destination; - } - } + $engineAdapter().prepareZipArgs(arguments); cfzip(attributeCollection="#arguments#"); } @@ -592,18 +582,7 @@ component output="false" { // this might fail if a query contains binary data so in those rare cases we fall back on using cfwddx (which is a little bit slower which is why we don't use it all the time) try { local.rv = SerializeJSON(local.values); - // BoxLang compatibility: For consistent hashing, normalize array representations - if (structKeyExists(server, "boxlang")) { - // Convert to a more predictable format by removing structural chars and sorting - local.normalized = REReplace(local.rv, '[\[\]{}"]', "", "all"); - local.parts = listToArray(local.normalized, ","); - arraySort(local.parts, "textnocase"); - local.rv = arrayToList(local.parts, ","); - } else { - // remove the characters that indicate array or struct so that we can sort it as a list below - local.rv = ReplaceList(local.rv, "{,},[,],/", ",,,,"); - local.rv = ListSort(local.rv, "text"); - } + local.rv = $engineAdapter().normalizeForHash(local.rv); } catch (any e) { local.rv = $wddx(input = local.values); } @@ -2281,16 +2260,7 @@ component output="false" { } } if (StructKeyExists(application.wheels.functions, arguments.name)) { - if (structKeyExists(server, "boxlang")) { - // Manual implementation for BoxLang - for (local.key in application.wheels.functions[arguments.name]) { - if (!StructKeyExists(arguments.args, local.key)) { - arguments.args[local.key] = application.wheels.functions[arguments.name][local.key]; - } - } - } else { - StructAppend(arguments.args, application.wheels.functions[arguments.name], false); - } + $engineAdapter().structAppendDefaults(arguments.args, application.wheels.functions[arguments.name]); } // make sure that the arguments marked as required exist @@ -2337,20 +2307,16 @@ component output="false" { local.val = arguments.value; local.detectedType = arguments.type; - // BoxLang sometimes returns oracle.sql.TIMESTAMP objects that aren't recognized as CFML date objects. - if ( - (StructKeyExists(server, "boxlang") || StructKeyExists(server, "coldfusion")) && - IsObject(local.val) && - FindNoCase("oracle.sql.TIMESTAMP", GetMetadata(local.val).name) - ) { - try { - // Safely convert it to a CFML date using its toString() method, which returns an ISO-like string - local.val = ParseDateTime(local.val.toString()); - local.detectedType = "datetime"; - } catch (any e) { - // Fallback: just get the string representation - local.val = local.val.toString(); - local.detectedType = "string"; + // Coerce Oracle JDBC objects (TIMESTAMP, DATE) to CFML datetime values. + if (IsObject(local.val)) { + local.coerced = $engineAdapter().coerceOracleObject(local.val); + if (!IsObject(local.coerced) || local.coerced.hashCode() != local.val.hashCode()) { + local.val = local.coerced; + if (IsDate(local.val)) { + local.detectedType = "datetime"; + } else { + local.detectedType = "string"; + } } } @@ -2394,9 +2360,9 @@ component output="false" { } } - // BoxLang compatibility: Pre-process problematic date strings before type detection - if (StructKeyExists(server, "boxlang") && IsSimpleValue(arguments.value) && ReFindNoCase("^\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2} (AM|PM)$", arguments.value)) { - // Manually parse DD/MM/YYYY format to avoid BoxLang's MM/DD/YYYY interpretation + // Pre-process date strings with AM/PM that may be parsed differently per engine + if ($engineAdapter().isBoxLang() && IsSimpleValue(arguments.value) && ReFindNoCase("^\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2} (AM|PM)$", arguments.value)) { + // Manually parse DD/MM/YYYY format to avoid engine-specific interpretation local.parts = ListToArray(arguments.value, " "); local.datePart = local.parts[1]; local.timePart = local.parts[2]; @@ -2416,9 +2382,7 @@ component output="false" { } else if (local.amPm == "AM" && local.hour == 12) { local.hour = 0; } - // convert to a real datetime object and continue (so switch will format it) val = CreateDateTime(local.year, local.month, local.day, local.hour, local.minute, 0); - // ensure detectedType is datetime so switch will format detectedType = "datetime"; } @@ -2503,12 +2467,10 @@ component output="false" { // likely MM/DD/YYYY local.month = d1; local.day = d2; } else { - // ambiguous -> prefer DD/MM/YYYY if server.boxlang exists, else MM/DD/YYYY - if (StructKeyExists(server, "boxlang")) { - local.day = d1; local.month = d2; - } else { - local.month = d1; local.day = d2; - } + // ambiguous -> use adapter to determine date format preference + local.ambiguousDate = $engineAdapter().parseAmbiguousSlashDate(d1, d2, y); + local.month = Month(local.ambiguousDate); + local.day = Day(local.ambiguousDate); } local.dt = CreateDate(y, local.month, local.day); // if time exists in same string, try to parse it using ParseDateTime diff --git a/vendor/wheels/Model.cfc b/vendor/wheels/Model.cfc index fa436cea9d..45105f407e 100644 --- a/vendor/wheels/Model.cfc +++ b/vendor/wheels/Model.cfc @@ -642,9 +642,7 @@ component output="false" displayName="Model" extends="wheels.Global"{ } function onDIcomplete(){ - if (structKeyExists(server, "boxlang")) { - variables.this = this; - } + $engineAdapter().prepareDIComplete(variables, this); new wheels.Plugins().$initializeMixins(variables); } } diff --git a/vendor/wheels/controller/rendering.cfc b/vendor/wheels/controller/rendering.cfc index 814ddd0f1c..3556448cbe 100644 --- a/vendor/wheels/controller/rendering.cfc +++ b/vendor/wheels/controller/rendering.cfc @@ -510,13 +510,12 @@ component { local.fileName = Replace("_" & local.fileName, "__", "_", "one"); } - // BoxLang compatibility: Extract folder name more reliably - if (structKeyExists(server, "boxlang")) { - // For BoxLang, extract folder path manually to handle version differences + // Engine compatibility: Extract folder name reliably + if ($engineAdapter().isBoxLang()) { + // Extract folder path manually to handle engine version differences local.tempName = arguments.$name; if (Find("/", local.tempName)) { local.folderName = Left(local.tempName, Len(local.tempName) - Len(ListLast(local.tempName, "/"))); - // Remove trailing slash if present, but only if there's more than just "/" if (Right(local.folderName, 1) == "/" AND Len(local.folderName) > 1) { local.folderName = Left(local.folderName, Len(local.folderName) - 1); } diff --git a/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc b/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc index b9f9db5708..6af27023de 100644 --- a/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc +++ b/vendor/wheels/engineAdapters/Adobe/AdobeAdapter.cfc @@ -1,11 +1,16 @@ /** * Engine adapter for Adobe ColdFusion. - * Overrides response access (FusionContext) and request timeout (RequestMonitor). + * Overrides response access (FusionContext), request timeout (RequestMonitor), + * and Oracle TIMESTAMP handling for $convertToString. */ component extends="wheels.engineAdapters.Base" output="false" { variables.engineName = "Adobe ColdFusion"; + public boolean function isAdobe() { + return true; + } + /** * Adobe CF requires getFusionContext() to access the response object. */ @@ -20,4 +25,27 @@ component extends="wheels.engineAdapters.Base" output="false" { return CreateObject("java", "coldfusion.runtime.RequestMonitor").GetRequestTimeout(); } + /** + * Adobe CF also needs Oracle TIMESTAMP coercion in $convertToString. + * (Both Adobe and BoxLang encounter oracle.sql.TIMESTAMP objects.) + */ + public any function coerceOracleObject(required any value) { + if (!IsObject(arguments.value) || IsStruct(arguments.value)) { + return arguments.value; + } + try { + local.className = GetMetadata(arguments.value).getName(); + } catch (any e) { + return arguments.value; + } + if (local.className == "oracle.sql.TIMESTAMP" || local.className == "oracle.sql.DATE") { + try { + return ParseDateTime(arguments.value.toString()); + } catch (any e) { + return arguments.value.toString(); + } + } + return arguments.value; + } + } diff --git a/vendor/wheels/engineAdapters/Base.cfc b/vendor/wheels/engineAdapters/Base.cfc index 309bd2b8f0..ab7437e592 100644 --- a/vendor/wheels/engineAdapters/Base.cfc +++ b/vendor/wheels/engineAdapters/Base.cfc @@ -31,6 +31,27 @@ component output="false" { return variables.engineMajorVersion; } + /** + * Returns true if the current engine is BoxLang. + */ + public boolean function isBoxLang() { + return false; + } + + /** + * Returns true if the current engine is Lucee. + */ + public boolean function isLucee() { + return false; + } + + /** + * Returns true if the current engine is Adobe ColdFusion. + */ + public boolean function isAdobe() { + return false; + } + // --- Response / PageContext --- /** @@ -109,4 +130,203 @@ component output="false" { return arguments.name; } + // --- Oracle JDBC Object Handling --- + + /** + * Coerces Oracle JDBC objects (TIMESTAMP, DATE) to CFML datetime values, + * and Oracle BLOB to binary data. Returns the value unchanged if it's not + * an Oracle JDBC object or if the engine doesn't need coercion. + * + * @value The value to check and potentially coerce + */ + public any function coerceOracleObject(required any value) { + return arguments.value; + } + + /** + * Returns true if the value is an Oracle JDBC object (TIMESTAMP, DATE) + * that should be treated as having content for validation purposes. + * + * @value The value to check + */ + public boolean function isOracleJdbcObject(required any value) { + return false; + } + + // --- Dynamic Finders --- + + /** + * Parses a dynamic finder method name (e.g. "findAllByTitleAndStatus") + * into an array of property names (e.g. ["Title", "Status"]). + * Lucee uppercases method names, so the Lucee adapter normalizes casing. + * + * @methodName The dynamic finder method name + * @prefix The prefix to strip ("findAllBy" or "findOneBy") + */ + public array function dynamicFinderProperties(required string methodName, required string prefix) { + return ListToArray( + ReplaceNoCase( + Replace(arguments.methodName, "And", "|", "all"), + arguments.prefix, "", "all" + ), + "|" + ); + } + + // --- Hash Normalization --- + + /** + * Normalizes a serialized JSON string for consistent cross-engine hashing. + * Removes structural characters and sorts the result. + * + * @serialized The serialized JSON string + */ + public string function normalizeForHash(required string serialized) { + local.rv = ReplaceList(arguments.serialized, "{,},[,],/", ",,,,"); + return ListSort(local.rv, "text"); + } + + // --- Struct Defaults --- + + /** + * Appends default values from a source struct to a target struct, + * only for keys that don't already exist in the target. + * + * @target The struct to append defaults to + * @defaults The struct containing default values + */ + public void function structAppendDefaults(required struct target, required struct defaults) { + StructAppend(arguments.target, arguments.defaults, false); + } + + // --- Numeric Validation --- + + /** + * Returns true if the value is a valid number. Stricter than IsNumeric() + * on engines where locale-aware parsing accepts commas. + * + * @value The value to check + */ + public boolean function isNumericStrict(required any value) { + return IsNumeric(arguments.value); + } + + // --- DI Completion --- + + /** + * Prepares the variables scope for DI completion and mixin injection. + * BoxLang requires `variables.this = this` before mixin integration. + * + * @vars The variables scope of the component + * @thisScope The this scope of the component + */ + public void function prepareDIComplete(required struct vars, required any thisScope) { + // Default: no-op. BoxLang overrides to set variables.this = this. + } + + // --- Method Invocation --- + + /** + * Invokes a public method on an object by name. + * BoxLang requires extracting the method reference and calling directly; + * Lucee/Adobe use the invoke() BIF. + * + * @object The object containing the method + * @methodName The name of the method to invoke + */ + public void function invokeMethod(required any object, required string methodName) { + invoke(object=arguments.object, method=arguments.methodName); + } + + // --- Image Handling --- + + /** + * Gets image information for a given source file. + * BoxLang uses ImageRead+ImageInfo; Lucee/Adobe use cfimage action=info. + * + * @source The path to the image file + */ + public struct function imageInfo(required string source) { + local.rv = {}; + local.args = {action: "info", source: arguments.source, structName: "rv"}; + cfimage(attributeCollection = local.args); + return local.rv; + } + + // --- Zip Handling --- + + /** + * Prepares zip arguments for the current engine. + * BoxLang requires absolute paths for file and destination. + * + * @args The arguments struct to prepare + */ + public struct function prepareZipArgs(required struct args) { + return arguments.args; + } + + // --- Glob Pattern Matching --- + + /** + * Returns the regex pattern for matching glob variables in route patterns. + * BoxLang uses *[varname] syntax; others use *varname. + */ + public string function globRegex() { + return "\*([^\/]+)"; + } + + /** + * Extracts the variable name from a glob match. + * + * @glob The matched glob string (e.g., "*varname" or "*[varname]") + */ + public string function extractGlobVariable(required string glob) { + return ReplaceList(arguments.glob, "*,[,]", ""); + } + + // --- Query Argument Mapping --- + + /** + * Returns the correct argument name for key column in cfquery. + * BoxLang uses "columnKey" instead of "keyColumn". + */ + public string function queryKeyColumnArgName() { + return "keyColumn"; + } + + // --- Port Detection --- + + /** + * Returns the default HTTP port for the current engine. + */ + public numeric function getDefaultPort() { + return 8500; + } + + // --- Date Parsing --- + + /** + * Parses an ambiguous slash-format date string (e.g. "01/02/2024") + * into a consistent date. When the day/month are ambiguous (both <= 12), + * prefers MM/DD/YYYY on Lucee/Adobe and DD/MM/YYYY on BoxLang. + * + * @d1 The first number (before first slash) + * @d2 The second number (between slashes) + * @year The year + */ + public date function parseAmbiguousSlashDate(required numeric d1, required numeric d2, required numeric year) { + // Default: MM/DD/YYYY (US format, Lucee/Adobe convention) + return CreateDate(arguments.year, arguments.d1, arguments.d2); + } + + // --- Readable Image Formats --- + + /** + * Returns readable image formats as a display string. + * BoxLang returns an array from GetReadableImageFormats(), so converts it. + */ + public string function getReadableImageFormatsString() { + return GetReadableImageFormats(); + } + } diff --git a/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc b/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc index cc12bc5c54..59f573397d 100644 --- a/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc +++ b/vendor/wheels/engineAdapters/BoxLang/BoxLangAdapter.cfc @@ -1,12 +1,17 @@ /** * Engine adapter for BoxLang. * BoxLang has significant differences in PageContext, form parsing, - * and controller name handling compared to Lucee/Adobe CF. + * controller name handling, Oracle JDBC objects, date parsing, + * and method invocation compared to Lucee/Adobe CF. */ component extends="wheels.engineAdapters.Base" output="false" { variables.engineName = "BoxLang"; + public boolean function isBoxLang() { + return true; + } + /** * BoxLang returns the response directly from GetPageContext(). */ @@ -67,4 +72,191 @@ component extends="wheels.engineAdapters.Base" output="false" { return local.dotPrefix & local.cleanName; } + // --- Oracle JDBC Object Handling --- + + /** + * BoxLang encounters Oracle JDBC objects (TIMESTAMP, DATE, BLOB) that need + * coercion to CFML-native types. + */ + public any function coerceOracleObject(required any value) { + if (!IsObject(arguments.value) || IsStruct(arguments.value)) { + return arguments.value; + } + try { + local.className = GetMetadata(arguments.value).getName(); + } catch (any e) { + return arguments.value; + } + if (local.className == "oracle.sql.TIMESTAMP" || local.className == "oracle.sql.DATE") { + try { + local.timestampString = arguments.value.toString(); + if (Len(local.timestampString)) { + return ParseDateTime(local.timestampString); + } + } catch (any e) { + try { + return arguments.value.toString(); + } catch (any e2) { + // fall through + } + } + return arguments.value; + } + if (local.className == "oracle.sql.BLOB") { + try { + return arguments.value.getBytes(); + } catch (any e) { + // fall through + } + } + return arguments.value; + } + + /** + * Returns true if the value is an Oracle JDBC object that should be + * treated as having content for validation purposes. + */ + public boolean function isOracleJdbcObject(required any value) { + if (!IsObject(arguments.value) || IsStruct(arguments.value)) { + return false; + } + try { + local.className = GetMetadata(arguments.value).getName(); + return ListContains("oracle.sql.TIMESTAMP,oracle.sql.DATE", local.className); + } catch (any e) { + return false; + } + } + + // --- Hash Normalization --- + + /** + * BoxLang needs a different normalization approach for consistent hashing. + * Removes structural chars with regex and sorts parts. + */ + public string function normalizeForHash(required string serialized) { + local.normalized = REReplace(arguments.serialized, '[\[\]{}"]', "", "all"); + local.parts = listToArray(local.normalized, ","); + arraySort(local.parts, "textnocase"); + return arrayToList(local.parts, ","); + } + + // --- Struct Defaults --- + + /** + * BoxLang's StructAppend doesn't work correctly with overwrite=false, + * so we manually loop and set defaults. + */ + public void function structAppendDefaults(required struct target, required struct defaults) { + for (local.key in arguments.defaults) { + if (!StructKeyExists(arguments.target, local.key)) { + arguments.target[local.key] = arguments.defaults[local.key]; + } + } + } + + // --- Numeric Validation --- + + /** + * BoxLang's IsNumeric() is locale-aware and accepts commas (e.g. "1,000.00"). + * This stricter version rejects values with commas. + */ + public boolean function isNumericStrict(required any value) { + if (!IsNumeric(arguments.value)) { + return false; + } + if (IsSimpleValue(arguments.value) && Find(",", arguments.value)) { + return false; + } + return true; + } + + // --- DI Completion --- + + /** + * BoxLang requires variables.this = this before mixin integration. + */ + public void function prepareDIComplete(required struct vars, required any thisScope) { + arguments.vars.this = arguments.thisScope; + } + + // --- Method Invocation --- + + /** + * BoxLang requires extracting the method reference and calling directly + * rather than using invoke(). + */ + public void function invokeMethod(required any object, required string methodName) { + local.method = arguments.object[arguments.methodName]; + local.method(); + } + + // --- Image Handling --- + + /** + * BoxLang uses ImageRead+ImageInfo instead of cfimage action=info. + */ + public struct function imageInfo(required string source) { + var img = ImageRead(arguments.source); + return ImageInfo(img); + } + + // --- Zip Handling --- + + /** + * BoxLang requires absolute paths for zip operations. + */ + public struct function prepareZipArgs(required struct args) { + if (StructKeyExists(arguments.args, "file") && Left(arguments.args.file, 1) != "/") { + arguments.args.file = "/" & arguments.args.file; + } + if (StructKeyExists(arguments.args, "destination") && Left(arguments.args.destination, 1) != "/") { + arguments.args.destination = "/" & arguments.args.destination; + } + return arguments.args; + } + + // --- Glob Pattern Matching --- + + /** + * BoxLang uses *[varname] glob syntax instead of *varname. + */ + public string function globRegex() { + return "\*\[([^\]]+)\]"; + } + + /** + * Extracts variable name from BoxLang's *[varname] pattern. + */ + public string function extractGlobVariable(required string glob) { + return ReReplace(arguments.glob, "\*\[([^\]]+)\]", "\1"); + } + + // --- Query Argument Mapping --- + + /** + * BoxLang uses "columnKey" instead of "keyColumn" for cfquery. + */ + public string function queryKeyColumnArgName() { + return "columnKey"; + } + + // --- Date Parsing --- + + /** + * BoxLang prefers DD/MM/YYYY when the date is ambiguous. + */ + public date function parseAmbiguousSlashDate(required numeric d1, required numeric d2, required numeric year) { + return CreateDate(arguments.year, arguments.d2, arguments.d1); + } + + // --- Readable Image Formats --- + + /** + * BoxLang's GetReadableImageFormats() returns an array, not a string. + */ + public string function getReadableImageFormatsString() { + return ArrayToList(GetReadableImageFormats(), ", "); + } + } diff --git a/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc b/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc index 77123b4fc5..d897267378 100644 --- a/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc +++ b/vendor/wheels/engineAdapters/EngineAdapterInterface.cfc @@ -13,6 +13,9 @@ interface { public string function getName(); public string function getVersion(); public numeric function getMajorVersion(); + public boolean function isBoxLang(); + public boolean function isLucee(); + public boolean function isAdobe(); // --- Response / PageContext --- public any function getResponse(); @@ -27,4 +30,48 @@ interface { // --- Controller --- public string function controllerNameToUpperCamelCase(required string name); + // --- Oracle JDBC Object Handling --- + public any function coerceOracleObject(required any value); + public boolean function isOracleJdbcObject(required any value); + + // --- Dynamic Finders --- + public array function dynamicFinderProperties(required string methodName, required string prefix); + + // --- Hash Normalization --- + public string function normalizeForHash(required string serialized); + + // --- Struct Defaults --- + public void function structAppendDefaults(required struct target, required struct defaults); + + // --- Numeric Validation --- + public boolean function isNumericStrict(required any value); + + // --- DI Completion --- + public void function prepareDIComplete(required struct vars, required any thisScope); + + // --- Method Invocation --- + public void function invokeMethod(required any object, required string methodName); + + // --- Image Handling --- + public struct function imageInfo(required string source); + + // --- Zip Handling --- + public struct function prepareZipArgs(required struct args); + + // --- Glob Pattern Matching --- + public string function globRegex(); + public string function extractGlobVariable(required string glob); + + // --- Query Argument Mapping --- + public string function queryKeyColumnArgName(); + + // --- Port Detection --- + public numeric function getDefaultPort(); + + // --- Date Parsing --- + public date function parseAmbiguousSlashDate(required numeric d1, required numeric d2, required numeric year); + + // --- Readable Image Formats --- + public string function getReadableImageFormatsString(); + } diff --git a/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc b/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc index 8b9ea03f01..7e25660714 100644 --- a/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc +++ b/vendor/wheels/engineAdapters/Lucee/LuceeAdapter.cfc @@ -1,10 +1,41 @@ /** * Engine adapter for Lucee CFML. * Lucee is the primary target engine — most defaults in Base.cfc - * already match Lucee behavior. This adapter only sets the engine name. + * already match Lucee behavior. This adapter only overrides where + * Lucee differs from the common defaults. */ component extends="wheels.engineAdapters.Base" output="false" { variables.engineName = "Lucee"; + public boolean function isLucee() { + return true; + } + + /** + * Lucee uppercases method names passed to onMissingMethod, so we + * lowercase the property names after stripping the finder prefix. + */ + public array function dynamicFinderProperties(required string methodName, required string prefix) { + return ListToArray( + LCase( + ReplaceNoCase( + ReplaceNoCase( + ReplaceNoCase(arguments.methodName, "And", "|", "all"), + "findAllBy", "", "all" + ), + "findOneBy", "", "all" + ) + ), + "|" + ); + } + + /** + * Lucee default port is 60000 (when running in Docker/CommandBox). + */ + public numeric function getDefaultPort() { + return 60000; + } + } diff --git a/vendor/wheels/events/EventMethods.cfc b/vendor/wheels/events/EventMethods.cfc index fa340fc41a..1e4a7ad955 100644 --- a/vendor/wheels/events/EventMethods.cfc +++ b/vendor/wheels/events/EventMethods.cfc @@ -44,7 +44,7 @@ component extends="wheels.Global" { // Detect request format for format-specific error handling local.format = $getRequestFormat(); - if (structKeyExists(server, "boxlang")) { + if ($engineAdapter().isBoxLang()) { if (StructKeyExists(arguments.exception, "type") && Left(arguments.exception.type, 6) == "Wheels") { local.wheelsError = arguments.exception; } else if ( @@ -164,9 +164,7 @@ component extends="wheels.Global" { // Inject methods from plugins and packages directly to Application.cfc. if (!StructIsEmpty(application.wheels.mixins)) { - if (structKeyExists(server, "boxlang")) { - variables.this = this; - } + $engineAdapter().prepareDIComplete(variables, this); local.Mixins.$initializeMixins(variables); } diff --git a/vendor/wheels/events/onapplicationstart.cfc b/vendor/wheels/events/onapplicationstart.cfc index 8651dcd9c8..9297bd7140 100644 --- a/vendor/wheels/events/onapplicationstart.cfc +++ b/vendor/wheels/events/onapplicationstart.cfc @@ -254,9 +254,7 @@ component { // Allow developers to inject plugins and packages into the application variables scope. if (!StructIsEmpty(application.$wheels.mixins)) { - if (structKeyExists(server, "boxlang")) { - variables.this = this; - } + application.$wheels.engineAdapter.prepareDIComplete(variables, this); new wheels.Plugins().$initializeMixins(variables); } diff --git a/vendor/wheels/mapper/matching.cfc b/vendor/wheels/mapper/matching.cfc index d0a52a6a7d..cd6e184695 100644 --- a/vendor/wheels/mapper/matching.cfc +++ b/vendor/wheels/mapper/matching.cfc @@ -340,17 +340,11 @@ component { } // See if we have any globing in the pattern and if so add a constraint for each glob. - // BoxLang compatibility: Use different regex pattern to match globs - local.globRegex = StructKeyExists(server, "boxlang") ? "\*\[([^\]]+)\]" : "\*([^\/]+)"; + local.globRegex = $engineAdapter().globRegex(); if (ReFindNoCase(local.globRegex, arguments.pattern)) { local.globs = ReMatch(local.globRegex, arguments.pattern); for (local.glob in local.globs) { - // For BoxLang: extract variable name from *[varname] pattern - if (StructKeyExists(server, "boxlang")) { - local.var = ReReplace(local.glob, "\*\[([^\]]+)\]", "\1"); - } else { - local.var = ReplaceList(local.glob, "*,[,]", ""); - } + local.var = $engineAdapter().extractGlobVariable(local.glob); arguments.pattern = Replace(arguments.pattern, local.glob, "[#local.var#]"); arguments.constraints[local.var] = ".*"; } diff --git a/vendor/wheels/model/associations.cfc b/vendor/wheels/model/associations.cfc index 254fb393af..6936babfdc 100644 --- a/vendor/wheels/model/associations.cfc +++ b/vendor/wheels/model/associations.cfc @@ -224,7 +224,7 @@ component { * Registers a foreign key property with integer type for BoxLang compatibility. */ public void function $registerBoxLangForeignKey(required string propertyName) { - if (StructKeyExists(server, "boxlang") && !StructKeyExists(variables.wheels.class.properties, arguments.propertyName)) { + if ($engineAdapter().isBoxLang() && !StructKeyExists(variables.wheels.class.properties, arguments.propertyName)) { property(name = arguments.propertyName, type = "integer"); } } diff --git a/vendor/wheels/model/callbacks.cfc b/vendor/wheels/model/callbacks.cfc index 79e026108a..fff371bd22 100644 --- a/vendor/wheels/model/callbacks.cfc +++ b/vendor/wheels/model/callbacks.cfc @@ -311,13 +311,11 @@ component { if (!QueryKeyExists(arguments.collection, local.key)) { QueryAddColumn(arguments.collection, local.key, []); } - if (structKeyExists(server, "boxlang")) { - if (local.result[local.key] == "") { - continue; - } + if ($engineAdapter().isBoxLang() && local.result[local.key] == "") { + continue; } - arguments.collection[local.key][local.rowNumber] = $coerceOracleTimestamp(local.result[local.key]); + arguments.collection[local.key][local.rowNumber] = $engineAdapter().coerceOracleObject(local.result[local.key]); } } else if (IsBoolean(local.result) && !local.result) { // Break the loop and return false if the callback returned false. @@ -357,31 +355,9 @@ component { /** * Internal function. - * Converts Oracle TIMESTAMP/DATE objects to CFML DateTime values (BoxLang compatibility). + * Converts Oracle TIMESTAMP/DATE objects to CFML DateTime values via the engine adapter. */ public any function $coerceOracleTimestamp(required any value) { - if (!structKeyExists(server, "boxlang") || !IsObject(arguments.value) || IsStruct(arguments.value)) { - return arguments.value; - } - try { - local.className = GetMetadata(arguments.value).getName(); - } catch (any e) { - return arguments.value; - } - if (local.className != "oracle.sql.TIMESTAMP" && local.className != "oracle.sql.DATE") { - return arguments.value; - } - local.timestampString = arguments.value.toString(); - local.dateParts = ListToArray(local.timestampString, " "); - local.dateComponents = ListToArray(local.dateParts[1], "-"); - local.timeComponents = ListToArray(local.dateParts[2], ":"); - return CreateDateTime( - Val(local.dateComponents[1]), - Val(local.dateComponents[2]), - Val(local.dateComponents[3]), - Val(local.timeComponents[1]), - Val(local.timeComponents[2]), - Val(ListFirst(local.timeComponents[3], ".")) - ); + return $engineAdapter().coerceOracleObject(arguments.value); } } diff --git a/vendor/wheels/model/create.cfc b/vendor/wheels/model/create.cfc index 012d03052b..6ba38ed5b7 100644 --- a/vendor/wheels/model/create.cfc +++ b/vendor/wheels/model/create.cfc @@ -335,11 +335,7 @@ component { $clearRequestCache(); local.generatedKey = variables.wheels.class.adapter.$generatedKey(); if (StructKeyExists(local.inserted.result, local.generatedKey)) { - if (StructKeyExists(server, "boxlang")) { - if (!local.primaryKeyExplicitlySet) { - this[primaryKeys(1)] = local.inserted.result[local.generatedKey]; - } - } else { + if (!$engineAdapter().isBoxLang() || !local.primaryKeyExplicitlySet) { this[primaryKeys(1)] = local.inserted.result[local.generatedKey]; } } diff --git a/vendor/wheels/model/miscellaneous.cfc b/vendor/wheels/model/miscellaneous.cfc index 96718986c5..0c42fffe5c 100644 --- a/vendor/wheels/model/miscellaneous.cfc +++ b/vendor/wheels/model/miscellaneous.cfc @@ -104,13 +104,11 @@ component { arguments.returnAs = "query"; arguments.callbacks = false; if (StructKeyExists(arguments, "key")) { - // BoxLang compatibility: Handle null key values - if (StructKeyExists(server, "boxlang") && (!StructKeyExists(arguments, "key") || arguments.key == "" || arguments.key == "null" || !Len(arguments.key))) { + if ($engineAdapter().isBoxLang() && (!StructKeyExists(arguments, "key") || arguments.key == "" || arguments.key == "null" || !Len(arguments.key))) { local.rv = 0; } else { local.result = findByKey(argumentCollection = arguments); - // BoxLang compatibility: Handle case where findByKey returns boolean false for null keys - if (StructKeyExists(server, "boxlang") && IsBoolean(local.result)) { + if (IsBoolean(local.result) && !local.result) { local.rv = 0; } else { local.rv = local.result.recordCount; @@ -118,8 +116,7 @@ component { } } else { local.result = findOne(argumentCollection = arguments); - // BoxLang compatibility: Handle case where findOne returns boolean false - if (StructKeyExists(server, "boxlang") && IsBoolean(local.result)) { + if (IsBoolean(local.result) && !local.result) { local.rv = 0; } else { local.rv = local.result.recordCount; @@ -286,17 +283,16 @@ component { local.rv.scale = variables.wheels.class.properties[arguments.property].scale; local.rv.null = (!Len(this[arguments.property]) && variables.wheels.class.properties[arguments.property].nullable); - // BoxLang/JDK compatibility: Convert date strings to proper date for datetime types - if (structKeyExists(server, "boxlang") && (Len(local.rv.value) && !local.rv.null && + // Convert date strings to proper date for datetime types (engine-specific parsing) + if ($engineAdapter().isBoxLang() && (Len(local.rv.value) && !local.rv.null && (local.rv.type == "CF_SQL_DATE" || local.rv.type == "CF_SQL_TIME" || local.rv.type == "CF_SQL_TIMESTAMP") && IsSimpleValue(local.rv.value) && !IsDate(local.rv.value))) { - // Handle DD/MM/YYYY and DD-MM-YYYY formats common in European locales if (REFind("^\d{1,2}[\/\-]\d{1,2}[\/\-]\d{4}$", local.rv.value)) { local.parts = ListToArray(local.rv.value, "/-"); if (ArrayLen(local.parts) == 3 && IsNumeric(local.parts[1]) && IsNumeric(local.parts[2]) && IsNumeric(local.parts[3])) { try { - local.rv.value = CreateDate(local.parts[3], local.parts[2], local.parts[1]); + local.rv.value = $engineAdapter().parseAmbiguousSlashDate(local.parts[1], local.parts[2], local.parts[3]); } catch (any e) { local.rv.value = CreateDate(local.parts[3], local.parts[1], local.parts[2]); } diff --git a/vendor/wheels/model/onmissingmethod.cfc b/vendor/wheels/model/onmissingmethod.cfc index f4e429d235..87f411a459 100644 --- a/vendor/wheels/model/onmissingmethod.cfc +++ b/vendor/wheels/model/onmissingmethod.cfc @@ -116,12 +116,8 @@ component { || Left(arguments.missingMethodName, 9) == "findAllBy" ) { // cfformat-ignore-start - if (StructKeyExists(server, "lucee")) { - // since Lucee passes in the method name in all upper case we have to do this here - local.finderProperties = ListToArray(LCase(ReplaceNoCase(ReplaceNoCase(ReplaceNoCase(arguments.missingMethodName, "And", "|", "all"), "findAllBy", "", "all"), "findOneBy", "", "all")), "|"); - } else { - local.finderProperties = ListToArray(ReplaceNoCase(ReplaceNoCase(Replace(arguments.missingMethodName, "And", "|", "all"), "findAllBy", "", "all"), "findOneBy", "", "all"), "|"); - } + local.finderPrefix = Left(arguments.missingMethodName, 9) == "findOneBy" ? "findOneBy" : "findAllBy"; + local.finderProperties = $engineAdapter().dynamicFinderProperties(arguments.missingMethodName, local.finderPrefix); // cfformat-ignore-end // sometimes values will have commas in them, allow the developer to change the delimiter @@ -172,19 +168,10 @@ component { // construct where clause local.addToWhere = ArrayToList(local.addToWhere, " AND "); - // BoxLang compatibility: Use explicit if/else instead of IIf for string concatenation - if (StructKeyExists(server, "boxlang")) { - if (StructKeyExists(arguments.missingMethodArguments, "where") && Len(arguments.missingMethodArguments.where)) { - arguments.missingMethodArguments.where = "(" & arguments.missingMethodArguments.where & ") AND (" & local.addToWhere & ")"; - } else { - arguments.missingMethodArguments.where = local.addToWhere; - } + if (StructKeyExists(arguments.missingMethodArguments, "where") && Len(arguments.missingMethodArguments.where)) { + arguments.missingMethodArguments.where = "(" & arguments.missingMethodArguments.where & ") AND (" & local.addToWhere & ")"; } else { - arguments.missingMethodArguments.where = IIf( - StructKeyExists(arguments.missingMethodArguments, "where") && Len(arguments.missingMethodArguments.where), - "'(' & arguments.missingMethodArguments.where & ') AND (' & local.addToWhere & ')'", - "local.addToWhere" - ); + arguments.missingMethodArguments.where = local.addToWhere; } // remove unneeded arguments @@ -194,20 +181,10 @@ component { StructDelete(arguments.missingMethodArguments, "values"); // call finder method - if (StructKeyExists(server, "boxlang")) { - // BoxLang-specific handling to avoid argumentCollection parsing issues - if (Left(arguments.missingMethodName, 9) == "findOneBy") { - local.rv = findOne(argumentCollection = arguments.missingMethodArguments); - } else { - local.rv = findAll(argumentCollection = arguments.missingMethodArguments); - } + if (Left(arguments.missingMethodName, 9) == "findOneBy") { + local.rv = findOne(argumentCollection = arguments.missingMethodArguments); } else { - // Adobe ColdFusion and Lucee compatibility - local.rv = IIf( - Left(arguments.missingMethodName, 9) == "findOneBy", - "findOne(argumentCollection=arguments.missingMethodArguments)", - "findAll(argumentCollection=arguments.missingMethodArguments)" - ); + local.rv = findAll(argumentCollection = arguments.missingMethodArguments); } } else if (Left(arguments.missingMethodName, 14) == "findOrCreateBy") { local.rv = $findOrCreateBy(argumentCollection = arguments); diff --git a/vendor/wheels/model/properties.cfc b/vendor/wheels/model/properties.cfc index e0eadd3d87..86a6052e51 100644 --- a/vendor/wheels/model/properties.cfc +++ b/vendor/wheels/model/properties.cfc @@ -582,19 +582,10 @@ component { } /** - * Resolves an object value, converting Oracle BLOB objects to binary in BoxLang. + * Resolves an object value, converting Oracle JDBC objects via the engine adapter. */ public any function $resolveObjectValue(required any value) { - if (StructKeyExists(server, "boxlang") && !IsStruct(arguments.value)) { - try { - if (GetMetadata(arguments.value).getName() == "oracle.sql.BLOB") { - return arguments.value.getBytes(); - } - } catch (any e) { - // If getMetadata fails, return as-is - } - } - return arguments.value; + return $engineAdapter().coerceOracleObject(arguments.value); } /** diff --git a/vendor/wheels/model/read.cfc b/vendor/wheels/model/read.cfc index 867f11c0e4..cee79ad134 100644 --- a/vendor/wheels/model/read.cfc +++ b/vendor/wheels/model/read.cfc @@ -321,9 +321,8 @@ component { // optional arguments for (local.argumentName in ["returnType","keyColumn"]) { if (StructKeyExists(arguments, local.argumentName)) { - // BoxLang compatibility: Map keyColumn to columnKey - if (StructKeyExists(server, "boxlang") && local.argumentName == "keyColumn") { - local.finderArgs["columnKey"] = arguments[local.argumentName]; + if (local.argumentName == "keyColumn") { + local.finderArgs[$engineAdapter().queryKeyColumnArgName()] = arguments[local.argumentName]; } else { local.finderArgs[local.argumentName] = arguments[local.argumentName]; } @@ -411,7 +410,7 @@ component { arguments.include = $listClean(arguments.include); if (Len(arguments.key)) { $keyLengthCheck(arguments.key); - } else if (structKeyExists(server, "boxlang")) { + } else if ($engineAdapter().isBoxLang()) { return false; } diff --git a/vendor/wheels/model/sql.cfc b/vendor/wheels/model/sql.cfc index 9802ba8648..021064f165 100644 --- a/vendor/wheels/model/sql.cfc +++ b/vendor/wheels/model/sql.cfc @@ -884,21 +884,17 @@ component { local.start = local.temp.pos[4] + local.temp.len[4]; local.extractedValue = Mid(arguments.where, local.temp.pos[4], local.temp.len[4]); - // BoxLang compatibility: Handle comma-separated values in IN clauses differently - if (StructKeyExists(server, "boxlang")) { + // Handle comma-separated values in IN clauses + if ($engineAdapter().isBoxLang()) { local.processedValue = local.extractedValue; - // Remove outer parentheses if present (e.g., "(1,2,3)" -> "1,2,3") if (Left(local.processedValue, 1) == "(" && Right(local.processedValue, 1) == ")") { local.processedValue = Mid(local.processedValue, 2, Len(local.processedValue) - 2); } - - // BoxLang: Only apply quote cleanup if the value contains quotes if (Find("'", local.processedValue) > 0 || Find(Chr(34), local.processedValue) > 0) { local.cleanedValue = local.processedValue; local.cleanedValue = ReReplace(local.cleanedValue, "'([^']*)'", "\1", "ALL"); local.doubleQuote = Chr(34); local.cleanedValue = ReReplace(local.cleanedValue, "#local.doubleQuote#([^#local.doubleQuote#]*)#local.doubleQuote#", "\1", "ALL"); - ArrayAppend(local.originalValues, local.cleanedValue); } else { ArrayAppend(local.originalValues, local.processedValue); diff --git a/vendor/wheels/model/validations.cfc b/vendor/wheels/model/validations.cfc index ab2c17803c..ddbfe4936b 100644 --- a/vendor/wheels/model/validations.cfc +++ b/vendor/wheels/model/validations.cfc @@ -548,13 +548,8 @@ component { local.value = this[arguments.validation.args.property]; - // Oracle TIMESTAMP/DATE objects in BoxLang — always invoke - if ( - StructKeyExists(server, "boxlang") - && IsObject(local.value) - && !IsStruct(local.value) - && ListContains("oracle.sql.TIMESTAMP,oracle.sql.DATE", GetMetadata(local.value).getName()) - ) { + // Oracle TIMESTAMP/DATE objects — always invoke validation + if ($engineAdapter().isOracleJdbcObject(local.value)) { return true; } @@ -701,11 +696,7 @@ component { local.value = this[arguments.property]; local.isValidNumber = IsNumeric(local.value); - // BoxLang compatibility — reject numbers with commas - // (BoxLang's IsNumeric is locale-aware and accepts "1,000.00") - if (StructKeyExists(server, "boxlang") && local.isValidNumber && Find(",", local.value)) { - local.isValidNumber = false; - } + local.isValidNumber = $engineAdapter().isNumericStrict(local.value); local.failed = !local.isValidNumber || (arguments.onlyInteger && Round(local.value) != local.value) diff --git a/vendor/wheels/tests/specs/engineAdapterSpec.cfc b/vendor/wheels/tests/specs/engineAdapterSpec.cfc index 7d4d7309c4..8c28d2e9dd 100644 --- a/vendor/wheels/tests/specs/engineAdapterSpec.cfc +++ b/vendor/wheels/tests/specs/engineAdapterSpec.cfc @@ -56,6 +56,46 @@ component extends="wheels.WheelsTest" { }); + describe("Engine Adapter - Identity Helpers", function() { + + it("reports exactly one engine identity as true", function() { + var adapter = application.wheels.engineAdapter; + var count = 0; + if (adapter.isLucee()) count++; + if (adapter.isAdobe()) count++; + if (adapter.isBoxLang()) count++; + expect(count).toBe(1); + }); + + it("identity matches engine name for Lucee", function() { + var adapter = application.wheels.engineAdapter; + if (adapter.getName() == "Lucee") { + expect(adapter.isLucee()).toBeTrue(); + expect(adapter.isAdobe()).toBeFalse(); + expect(adapter.isBoxLang()).toBeFalse(); + } + }); + + it("identity matches engine name for Adobe ColdFusion", function() { + var adapter = application.wheels.engineAdapter; + if (adapter.getName() == "Adobe ColdFusion") { + expect(adapter.isAdobe()).toBeTrue(); + expect(adapter.isLucee()).toBeFalse(); + expect(adapter.isBoxLang()).toBeFalse(); + } + }); + + it("identity matches engine name for BoxLang", function() { + var adapter = application.wheels.engineAdapter; + if (adapter.getName() == "BoxLang") { + expect(adapter.isBoxLang()).toBeTrue(); + expect(adapter.isLucee()).toBeFalse(); + expect(adapter.isAdobe()).toBeFalse(); + } + }); + + }); + describe("Engine Adapter - parseFormKey", function() { it("parses single-level bracket key", function() { @@ -100,6 +140,202 @@ component extends="wheels.WheelsTest" { }); + describe("Engine Adapter - Oracle JDBC Object Handling", function() { + + it("returns simple values unchanged from coerceOracleObject", function() { + expect(application.wheels.engineAdapter.coerceOracleObject("hello")).toBe("hello"); + }); + + it("returns numbers unchanged from coerceOracleObject", function() { + expect(application.wheels.engineAdapter.coerceOracleObject(42)).toBe(42); + }); + + it("returns structs unchanged from coerceOracleObject", function() { + var s = {foo: "bar"}; + var result = application.wheels.engineAdapter.coerceOracleObject(s); + expect(result.foo).toBe("bar"); + }); + + it("returns false for simple values from isOracleJdbcObject", function() { + expect(application.wheels.engineAdapter.isOracleJdbcObject("test")).toBeFalse(); + expect(application.wheels.engineAdapter.isOracleJdbcObject(123)).toBeFalse(); + }); + + it("returns false for structs from isOracleJdbcObject", function() { + expect(application.wheels.engineAdapter.isOracleJdbcObject({a: 1})).toBeFalse(); + }); + + }); + + describe("Engine Adapter - Dynamic Finders", function() { + + it("parses findAllByTitle into single property", function() { + var result = application.wheels.engineAdapter.dynamicFinderProperties("findAllByTitle", "findAllBy"); + expect(result).toBeArray(); + expect(ArrayLen(result)).toBe(1); + }); + + it("parses findOneByTitleAndStatus into two properties", function() { + var result = application.wheels.engineAdapter.dynamicFinderProperties("findOneByTitleAndStatus", "findOneBy"); + expect(result).toBeArray(); + expect(ArrayLen(result)).toBe(2); + }); + + it("parses findAllByFirstNameAndLastNameAndEmail into three properties", function() { + var result = application.wheels.engineAdapter.dynamicFinderProperties("findAllByFirstNameAndLastNameAndEmail", "findAllBy"); + expect(result).toBeArray(); + expect(ArrayLen(result)).toBe(3); + }); + + }); + + describe("Engine Adapter - Hash Normalization", function() { + + it("normalizes JSON for consistent hashing", function() { + var result = application.wheels.engineAdapter.normalizeForHash('{"b":"2","a":"1"}'); + expect(IsSimpleValue(result)).toBeTrue(); + expect(Len(result)).toBeGT(0); + }); + + it("produces deterministic output regardless of key order", function() { + var r1 = application.wheels.engineAdapter.normalizeForHash('["a","b","c"]'); + var r2 = application.wheels.engineAdapter.normalizeForHash('["c","b","a"]'); + expect(r1).toBe(r2); + }); + + }); + + describe("Engine Adapter - Struct Defaults", function() { + + it("appends missing keys from defaults", function() { + var target = {a: 1}; + var defaults = {a: 99, b: 2, c: 3}; + application.wheels.engineAdapter.structAppendDefaults(target, defaults); + expect(target.a).toBe(1); + expect(target.b).toBe(2); + expect(target.c).toBe(3); + }); + + it("does not overwrite existing keys", function() { + var target = {name: "original"}; + var defaults = {name: "default", extra: "value"}; + application.wheels.engineAdapter.structAppendDefaults(target, defaults); + expect(target.name).toBe("original"); + expect(target.extra).toBe("value"); + }); + + }); + + describe("Engine Adapter - Numeric Validation", function() { + + it("returns true for simple integers", function() { + expect(application.wheels.engineAdapter.isNumericStrict(42)).toBeTrue(); + }); + + it("returns true for decimals", function() { + expect(application.wheels.engineAdapter.isNumericStrict(3.14)).toBeTrue(); + }); + + it("returns true for negative numbers", function() { + expect(application.wheels.engineAdapter.isNumericStrict(-7)).toBeTrue(); + }); + + it("returns false for non-numeric strings", function() { + expect(application.wheels.engineAdapter.isNumericStrict("abc")).toBeFalse(); + }); + + }); + + describe("Engine Adapter - Glob Pattern Matching", function() { + + it("returns a non-empty glob regex", function() { + expect(Len(application.wheels.engineAdapter.globRegex())).toBeGT(0); + }); + + it("extracts variable name from glob on current engine", function() { + var adapter = application.wheels.engineAdapter; + if (adapter.isBoxLang()) { + var result = adapter.extractGlobVariable("*[myVar]"); + expect(result).toBe("myVar"); + } else { + var result = adapter.extractGlobVariable("*myVar"); + expect(result).toBe("myVar"); + } + }); + + }); + + describe("Engine Adapter - Query Argument Mapping", function() { + + it("returns a valid argument name for key column", function() { + var argName = application.wheels.engineAdapter.queryKeyColumnArgName(); + expect(ListFind("keyColumn,columnKey", argName)).toBeGT(0); + }); + + }); + + describe("Engine Adapter - Port Detection", function() { + + it("returns a numeric default port", function() { + expect(application.wheels.engineAdapter.getDefaultPort()).toBeNumeric(); + }); + + it("returns a reasonable port number", function() { + var port = application.wheels.engineAdapter.getDefaultPort(); + expect(port).toBeGT(0); + expect(port).toBeLT(100000); + }); + + }); + + describe("Engine Adapter - Date Parsing", function() { + + it("parses an unambiguous date correctly (day > 12)", function() { + // 25/03/2024 — day=25, month=3 + var result = application.wheels.engineAdapter.parseAmbiguousSlashDate(25, 3, 2024); + expect(IsDate(result)).toBeTrue(); + }); + + it("returns a date object", function() { + var result = application.wheels.engineAdapter.parseAmbiguousSlashDate(5, 7, 2024); + expect(IsDate(result)).toBeTrue(); + }); + + }); + + describe("Engine Adapter - Image Formats", function() { + + it("returns a non-empty image formats string", function() { + var result = application.wheels.engineAdapter.getReadableImageFormatsString(); + expect(IsSimpleValue(result)).toBeTrue(); + expect(Len(result)).toBeGT(0); + }); + + }); + + describe("Engine Adapter - DI Completion", function() { + + it("does not throw when called with empty struct", function() { + var vars = {}; + var thisScope = {}; + application.wheels.engineAdapter.prepareDIComplete(vars, thisScope); + // Should not throw - BoxLang sets vars.this, others no-op + expect(true).toBeTrue(); + }); + + }); + + describe("Engine Adapter - Zip Args", function() { + + it("returns the args struct", function() { + var args = {file: "test.zip", destination: "/tmp"}; + var result = application.wheels.engineAdapter.prepareZipArgs(args); + expect(IsStruct(result)).toBeTrue(); + expect(StructKeyExists(result, "file")).toBeTrue(); + }); + + }); + } } diff --git a/vendor/wheels/view/assets.cfc b/vendor/wheels/view/assets.cfc index b8a378b054..69c9c15f34 100644 --- a/vendor/wheels/view/assets.cfc +++ b/vendor/wheels/view/assets.cfc @@ -215,19 +215,11 @@ component { extendedInfo = "Pass in a correct relative path from the `images` folder to an image." ); } else if (!IsImageFile(local.file)) { - if (structKeyExists(server, "boxlang")) { - Throw( - type = "Wheels.ImageFormatNotSupported", - message = "Wheels can't read image files with that format.", - extendedInfo = "Use one of these image types instead: #ArrayToList(GetReadableImageFormats(), ', ')#." - ); - } else { - Throw( - type = "Wheels.ImageFormatNotSupported", - message = "Wheels can't read image files with that format.", - extendedInfo = "Use one of these image types instead: #GetReadableImageFormats()#." - ); - } + Throw( + type = "Wheels.ImageFormatNotSupported", + message = "Wheels can't read image files with that format.", + extendedInfo = "Use one of these image types instead: #$engineAdapter().getReadableImageFormatsString()#." + ); } } if (!StructKeyExists(arguments, "width") || !StructKeyExists(arguments, "height")) { diff --git a/vendor/wheels/view/formsdate.cfc b/vendor/wheels/view/formsdate.cfc index 9a6a9d8757..0c8ebbf9b5 100644 --- a/vendor/wheels/view/formsdate.cfc +++ b/vendor/wheels/view/formsdate.cfc @@ -124,8 +124,8 @@ component { } else if (isInstanceOf(local.value, "oracle.sql.TIMESTAMP")){ local.value = local.value.timestampValue(); } - if (structKeyExists(server, "boxlang") && IsSimpleValue(local.value) && Len(local.value)) { - // BoxLang compatibility: Fix date parsing issues + if ($engineAdapter().isBoxLang() && IsSimpleValue(local.value) && Len(local.value)) { + // Engine compatibility: Fix date parsing issues // Handle SQL Server format YYYY-MM-DD HH:MM:SS if (ReFindNoCase("^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", local.value)) { local.parts = ListToArray(Left(local.value, 10), "-"); diff --git a/vendor/wheels/view/links.cfc b/vendor/wheels/view/links.cfc index 3a554ba56e..49b8b6a50a 100644 --- a/vendor/wheels/view/links.cfc +++ b/vendor/wheels/view/links.cfc @@ -440,8 +440,7 @@ component { // Create anchor elements with an href attribute for all URLs found in the text. if (arguments.link != "emailAddresses") { - // For BoxLang compatibility - if (structKeyExists(server, "boxlang")) { + if ($engineAdapter().isBoxLang()) { local.anchors = []; local.tempText = arguments.text; local.anchorMatches = ReMatchNoCase("]*>.*?", local.tempText);