浏览代码

feat(expressions): add TypeAccessorRegistry and string accessors

Introduce a centralized TypeAccessorRegistry for resolving property
and function accessors for different types. Refactor
FunctionCallExpression and PropertyExpression to use the registry
instead of inline accessor resolution logic.

Add StringPropertyAccessor and StringFunctionAccessor to enable
string manipulation in expressions (length, empty, upper, lower,
contains, replace, split, etc.) with comprehensive test coverage.

Also simplify ValueElement.try_get_as to apply value transformation
uniformly for all types.
Billy Barrow 6 天之前
父节点
当前提交
897cdf3e70

+ 6 - 9
src/lib/Element.vala

@@ -127,7 +127,7 @@ namespace Invercargill {
 
     public class ValueElement : Object, Element {
 
-        public Value value { get; private set; }
+        private Value value;
 
         public ValueElement(Value value) {
             this.value = value;
@@ -162,19 +162,16 @@ namespace Invercargill {
                 return true;
             }
 
-            // For reference types, try to get the object
-            if(!requested_type.is_value_type() && value_type.is_a(requested_type)) {
+            if(value_type.is_a(requested_type)) {
                 result = (T)value.get_object();
                 return true;
             }
 
             // Try value transformation for reference types
-            if(!requested_type.is_value_type()) {
-                var converted = Value(requested_type);
-                if(value.transform(ref converted)) {
-                    result = (T)converted.get_object();
-                    return true;
-                }
+            var converted = Value(requested_type);
+            if(value.transform(ref converted)) {
+                result = (T)converted.get_object();
+                return true;
             }
 
             result = null;

+ 66 - 0
src/lib/Expressions/AccessorFactory.vala

@@ -0,0 +1,66 @@
+
+namespace Invercargill.Expressions {
+
+    /**
+     * Factory interface for creating PropertyAccessor instances for specific types.
+     * 
+     * Implementations of this interface are responsible for creating PropertyAccessor
+     * instances that can read properties from objects of a specific type.
+     * 
+     * Example usage:
+     * {{{
+     * public class StringPropertyAccessorFactory : Object, PropertyAccessorFactory {
+     *     public PropertyAccessor? create(Element element) {
+     *         string? value;
+     *         if (element.try_get_as(out value)) {
+     *             return new StringPropertyAccessor(value);
+     *         }
+     *         return null;
+     *     }
+     * }
+     * }}}
+     */
+    public interface PropertyAccessorFactory : Object {
+
+        /**
+         * Creates a PropertyAccessor for the given Element if applicable.
+         * 
+         * @param element The Element to create an accessor for
+         * @return A PropertyAccessor if this factory handles the element's type, null otherwise
+         */
+        public abstract PropertyAccessor? create(Element element);
+
+    }
+
+    /**
+     * Factory interface for creating FunctionAccessor instances for specific types.
+     * 
+     * Implementations of this interface are responsible for creating FunctionAccessor
+     * instances that can call functions on objects of a specific type.
+     * 
+     * Example usage:
+     * {{{
+     * public class StringFunctionAccessorFactory : Object, FunctionAccessorFactory {
+     *     public FunctionAccessor? create(Element element) {
+     *         string? value;
+     *         if (element.try_get_as(out value)) {
+     *             return new StringFunctionAccessor(value);
+     *         }
+     *         return null;
+     *     }
+     * }
+     * }}}
+     */
+    public interface FunctionAccessorFactory : Object {
+
+        /**
+         * Creates a FunctionAccessor for the given Element if applicable.
+         * 
+         * @param element The Element to create an accessor for
+         * @return A FunctionAccessor if this factory handles the element's type, null otherwise
+         */
+        public abstract FunctionAccessor? create(Element element);
+
+    }
+
+}

+ 0 - 1
src/lib/Expressions/Elements/LambdaElement.vala

@@ -63,7 +63,6 @@ namespace Invercargill.Expressions {
         public override string to_string() {
             return @"Lambda[$(_lambda.parameter_name) => $(_lambda.body.to_expression_string())]";
         }
-
     }
 
 }

+ 6 - 31
src/lib/Expressions/Expressions/FunctionCallExpression.vala

@@ -8,6 +8,9 @@ namespace Invercargill.Expressions {
      * Uses the FunctionAccessor interface to call functions on objects.
      * Supports passing arguments including lambdas.
      * 
+     * The TypeAccessorRegistry is consulted to find appropriate accessors
+     * for specific types.
+     * 
      * Example: `a.values.where(s => s.rank > 5)`
      */
     public class FunctionCallExpression : Object, Expression {
@@ -68,8 +71,9 @@ namespace Invercargill.Expressions {
         public Element evaluate(EvaluationContext context) throws ExpressionError {
             var target_value = target.evaluate(context);
 
-            // Get FunctionAccessor from the element
-            FunctionAccessor? accessor = get_function_accessor(target_value);
+            // Use the TypeAccessorRegistry to get an appropriate accessor
+            var registry = TypeAccessorRegistry.get_instance();
+            var accessor = registry.get_function_accessor(target_value);
 
             if (accessor == null) {
                 throw new ExpressionError.INVALID_TYPE(
@@ -114,35 +118,6 @@ namespace Invercargill.Expressions {
             return @"$(target.to_expression_string()).$function_name($(args_str.str))";
         }
 
-        // Private helper methods
-
-        private FunctionAccessor? get_function_accessor(Element element) {
-            // Check for GlobalFunctionsElement first
-            if (element is GlobalFunctionsElement) {
-                return ((GlobalFunctionsElement)element).accessor;
-            }
-
-            // Check if element directly implements FunctionAccessor
-            FunctionAccessor? accessor;
-            if (element.try_get_as(out accessor)) {
-                return accessor;
-            }
-
-            // Check for Elements (Enumerable wrapper)
-            Elements? elements;
-            if (element.try_get_as(out elements)) {
-                return new EnumerableFunctionAccessor(elements);
-            }
-
-            // Check for Enumerable directly
-            Enumerable? enumerable;
-            if (element.try_get_as(out enumerable)) {
-                return new EnumerableFunctionAccessor(enumerable.to_elements());
-            }
-
-            return null;
-        }
-
     }
 
 }

+ 9 - 18
src/lib/Expressions/Expressions/PropertyExpression.vala

@@ -5,8 +5,11 @@ namespace Invercargill.Expressions {
      * Expression that accesses a property on a target object.
      * 
      * Uses the PropertyAccessor interface to navigate properties on objects,
-     * supporting various target types including Object, Properties, and
-     * PropertyAccessor implementations.
+     * supporting various target types including Object, Properties, strings,
+     * and PropertyAccessor implementations.
+     * 
+     * The TypeAccessorRegistry is consulted to find appropriate accessors
+     * for specific types.
      */
     public class PropertyExpression : Object, Expression {
 
@@ -35,23 +38,11 @@ namespace Invercargill.Expressions {
         public Element evaluate(EvaluationContext context) throws ExpressionError {
             var target_value = target.evaluate(context);
 
-            // If the element itself is a PropertyAccessor
-            PropertyAccessor? accessor = null;
-            if (target_value.try_get_as(out accessor)) {
-                return accessor.read_property(property_name);
-            }
-
-            // If it's an Object, wrap it with ObjectPropertyAccessor
-            Object? obj;
-            if (target_value.try_get_as(out obj)) {
-                accessor = new ObjectPropertyAccessor(obj);
-                return accessor.read_property(property_name);
-            }
+            // Use the TypeAccessorRegistry to get an appropriate accessor
+            var registry = TypeAccessorRegistry.get_instance();
+            var accessor = registry.get_property_accessor(target_value);
 
-            // If it's a Properties instance, wrap it with PropertiesPropertyAccessor
-            Properties? props;
-            if (target_value.try_get_as(out props)) {
-                accessor = new PropertiesPropertyAccessor(props);
+            if (accessor != null) {
                 return accessor.read_property(property_name);
             }
 

+ 433 - 0
src/lib/Expressions/StringFunctionAccessor.vala

@@ -0,0 +1,433 @@
+using Invercargill.DataStructures;
+using Invercargill.Wrappers;
+
+namespace Invercargill.Expressions {
+
+    /**
+     * Function accessor for string types.
+     * 
+     * Provides access to string functions:
+     * - `chug()`: Removes leading whitespace
+     * - `chomp()`: Removes trailing whitespace
+     * - `upper()`: Converts to uppercase
+     * - `lower()`: Converts to lowercase
+     * - `reverse()`: Reverses the string
+     * - `substring(start, len)`: Extracts a substring
+     * - `contains(substring)`: Checks if contains substring
+     * - `has_prefix(prefix)`: Checks if starts with prefix
+     * - `has_suffix(suffix)`: Checks if ends with suffix
+     * - `replace(old, new)`: Replaces occurrences of old with new
+     * - `split(separator)`: Splits string into Enumerable<string>
+     * - `char_at(index)`: Gets character at index
+     * 
+     * Example usage in expressions:
+     * {{{
+     * name.upper()                    // "hello" → "HELLO"
+     * name.contains("ell")            // "hello" → true
+     * name.substring(1, 3)            // "hello" → "ell"
+     * name.split(",")                 // "a,b,c" → Enumerable<string>
+     * }}}
+     */
+    public class StringFunctionAccessor : Object, FunctionAccessor {
+
+        /**
+         * The string value being accessed.
+         */
+        private string? _value;
+
+        /**
+         * Creates a new StringFunctionAccessor.
+         * 
+         * @param value The string value to call functions on
+         */
+        public StringFunctionAccessor(string? value) {
+            _value = value;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public bool has_function(string function_name) {
+            switch (function_name) {
+                case "chug":
+                case "chomp":
+                case "upper":
+                case "lower":
+                case "reverse":
+                case "substring":
+                case "contains":
+                case "has_prefix":
+                case "has_suffix":
+                case "replace":
+                case "split":
+                case "char_at":
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Enumerable<string> get_function_names() {
+            return new Wrappers.Array<string>(new string[]{
+                "chug",
+                "chomp",
+                "upper",
+                "lower",
+                "reverse",
+                "substring",
+                "contains",
+                "has_prefix",
+                "has_suffix",
+                "replace",
+                "split",
+                "char_at"
+            });
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Element call_function(
+            string function_name,
+            Series<Element> arguments,
+            EvaluationContext context
+        ) throws ExpressionError {
+            
+            // Handle null string - most functions return null or empty
+            if (_value == null) {
+                return handle_null_function(function_name, arguments);
+            }
+
+            switch (function_name) {
+                case "chug":
+                    return call_chug(arguments);
+                
+                case "chomp":
+                    return call_chomp(arguments);
+                
+                case "upper":
+                    return call_upper(arguments);
+                
+                case "lower":
+                    return call_lower(arguments);
+                
+                case "reverse":
+                    return call_reverse(arguments);
+                
+                case "substring":
+                    return call_substring(arguments);
+                
+                case "contains":
+                    return call_contains(arguments);
+                
+                case "has_prefix":
+                    return call_has_prefix(arguments);
+                
+                case "has_suffix":
+                    return call_has_suffix(arguments);
+                
+                case "replace":
+                    return call_replace(arguments);
+                
+                case "split":
+                    return call_split(arguments);
+                
+                case "char_at":
+                    return call_char_at(arguments);
+                
+                default:
+                    throw new ExpressionError.FUNCTION_NOT_FOUND(
+                        @"Unknown string function: $function_name"
+                    );
+            }
+        }
+
+        /**
+         * Handles function calls on null strings.
+         */
+        private Element handle_null_function(string function_name, Series<Element> arguments) 
+            throws ExpressionError {
+            
+            switch (function_name) {
+                case "split":
+                    // Return empty Enumerable<string> for null split
+                    return new ValueElement(new Wrappers.Array<string>(new string[0]));
+                
+                default:
+                    // Most string functions return null when called on null
+                    return new NativeElement<string?>(null);
+            }
+        }
+
+        /**
+         * Removes leading whitespace.
+         */
+        private Element call_chug(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("chug", arguments, 0);
+            return new NativeElement<string?>(_value.chomp().chug());
+        }
+
+        /**
+         * Removes trailing whitespace.
+         */
+        private Element call_chomp(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("chomp", arguments, 0);
+            return new NativeElement<string?>(_value.chomp());
+        }
+
+        /**
+         * Converts to uppercase.
+         */
+        private Element call_upper(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("upper", arguments, 0);
+            return new NativeElement<string?>(_value.up());
+        }
+
+        /**
+         * Converts to lowercase.
+         */
+        private Element call_lower(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("lower", arguments, 0);
+            return new NativeElement<string?>(_value.down());
+        }
+
+        /**
+         * Reverses the string.
+         */
+        private Element call_reverse(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("reverse", arguments, 0);
+            return new NativeElement<string?>(_value.reverse());
+        }
+
+        /**
+         * Extracts a substring.
+         */
+        private Element call_substring(Series<Element> arguments) throws ExpressionError {
+            var args = to_array(arguments);
+            
+            if (args.length == 1) {
+                // substring(start) - from start to end
+                int start = get_int_argument(args[0], "substring start");
+                if (start < 0) {
+                    start = _value.length + start;
+                }
+                if (start < 0 || start > _value.length) {
+                    return new NativeElement<string?>("");
+                }
+                return new NativeElement<string?>(_value.substring(start));
+            } else if (args.length == 2) {
+                // substring(start, length)
+                int start = get_int_argument(args[0], "substring start");
+                int len = get_int_argument(args[1], "substring length");
+                if (start < 0) {
+                    start = _value.length + start;
+                }
+                if (start < 0 || start > _value.length) {
+                    return new NativeElement<string?>("");
+                }
+                return new NativeElement<string?>(_value.substring(start, len));
+            } else {
+                throw new ExpressionError.INVALID_ARGUMENTS(
+                    @"substring expects 1 or 2 arguments, got $(args.length)"
+                );
+            }
+        }
+        
+        /**
+         * Helper to get an integer from an Element, handling various numeric types.
+         */
+        private int get_int_argument(Element element, string arg_name) throws ExpressionError {
+            // Try int first
+            int int_val;
+            if (element.try_get_as(out int_val)) {
+                return int_val;
+            }
+            // Try int64
+            int64? int64_val;
+            if (element.try_get_as(out int64_val)) {
+                return (int)int64_val;
+            }
+            // Try long
+            long? long_val;
+            if (element.try_get_as(out long_val)) {
+                return (int)long_val;
+            }
+            // Try double (for cases where someone passes a float)
+            double? double_val;
+            if (element.try_get_as(out double_val)) {
+                return (int)double_val;
+            }
+            throw new ExpressionError.INVALID_ARGUMENTS(
+                @"$arg_name must be an integer"
+            );
+        }
+
+        /**
+         * Checks if string contains substring.
+         */
+        private Element call_contains(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("contains", arguments, 1);
+            var args = to_array(arguments);
+            string? substring;
+            if (!args[0].try_get_as(out substring)) {
+                return new NativeElement<bool?>(false);
+            }
+            if (substring == null) {
+                return new NativeElement<bool?>(false);
+            }
+            return new NativeElement<bool?>(_value.contains(substring));
+        }
+
+        /**
+         * Checks if string starts with prefix.
+         */
+        private Element call_has_prefix(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("has_prefix", arguments, 1);
+            var args = to_array(arguments);
+            string? prefix;
+            if (!args[0].try_get_as(out prefix)) {
+                return new NativeElement<bool?>(false);
+            }
+            if (prefix == null) {
+                return new NativeElement<bool?>(false);
+            }
+            return new NativeElement<bool?>(_value.has_prefix(prefix));
+        }
+
+        /**
+         * Checks if string ends with suffix.
+         */
+        private Element call_has_suffix(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("has_suffix", arguments, 1);
+            var args = to_array(arguments);
+            string? suffix;
+            if (!args[0].try_get_as(out suffix)) {
+                return new NativeElement<bool?>(false);
+            }
+            if (suffix == null) {
+                return new NativeElement<bool?>(false);
+            }
+            return new NativeElement<bool?>(_value.has_suffix(suffix));
+        }
+
+        /**
+         * Replaces occurrences of old with new.
+         */
+        private Element call_replace(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("replace", arguments, 2);
+            var args = to_array(arguments);
+            string? old_str;
+            string? new_str;
+            if (!args[0].try_get_as(out old_str)) {
+                return new NativeElement<string?>(_value);
+            }
+            if (!args[1].try_get_as(out new_str)) {
+                new_str = "";
+            }
+            if (old_str == null) {
+                return new NativeElement<string?>(_value);
+            }
+            return new NativeElement<string?>(_value.replace(old_str, new_str ?? ""));
+        }
+
+        /**
+         * Splits string into Enumerable<string>.
+         */
+        private Element call_split(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("split", arguments, 1);
+            var args = to_array(arguments);
+            string? separator;
+            if (!args[0].try_get_as(out separator)) {
+                separator = null;
+            }
+            
+            if (separator == null || separator.length == 0) {
+                // Split by each character
+                var result = new string[_value.length];
+                for (int i = 0; i < _value.length; i++) {
+                    result[i] = _value.substring(i, 1);
+                }
+                return new ValueElement(new Wrappers.Array<string>(result));
+            }
+            
+            string[] parts = _value.split(separator);
+            return new ValueElement(new Wrappers.Array<string>(parts));
+        }
+
+        /**
+         * Gets character at index.
+         */
+        private Element call_char_at(Series<Element> arguments) throws ExpressionError {
+            expect_argument_count("char_at", arguments, 1);
+            var args = to_array(arguments);
+            int index = get_int_argument(args[0], "char_at index");
+            
+            if (index < 0) {
+                index = _value.length + index;
+            }
+            
+            if (index < 0 || index >= _value.length) {
+                return new NativeElement<string?>(null);
+            }
+            
+            return new NativeElement<string?>(_value.substring(index, 1));
+        }
+
+        /**
+         * Helper to validate argument count.
+         */
+        private void expect_argument_count(string function_name, Series<Element> arguments, int expected) 
+            throws ExpressionError {
+            int count = 0;
+            foreach (var _ in arguments) {
+                count++;
+            }
+            if (count != expected) {
+                throw new ExpressionError.INVALID_ARGUMENTS(
+                    @"$function_name expects $expected argument(s), got $count"
+                );
+            }
+        }
+
+        /**
+         * Helper to convert Series to array.
+         */
+        private Element[] to_array(Series<Element> arguments) {
+            var list = new GLib.GenericArray<Element>();
+            foreach (var arg in arguments) {
+                list.add(arg);
+            }
+            var result = new Element[list.length];
+            for (int i = 0; i < list.length; i++) {
+                result[i] = list[i];
+            }
+            return result;
+        }
+
+    }
+
+    /**
+     * Factory for creating StringFunctionAccessor instances.
+     * 
+     * This factory is registered with the TypeAccessorRegistry to provide
+     * function access for string values.
+     */
+    public class StringFunctionAccessorFactory : Object, FunctionAccessorFactory {
+
+        /**
+         * {@inheritDoc}
+         */
+        public FunctionAccessor? create(Element element) {
+            string? value;
+            if (element.try_get_as(out value)) {
+                return new StringFunctionAccessor(value);
+            }
+            return null;
+        }
+
+    }
+
+}

+ 123 - 0
src/lib/Expressions/StringPropertyAccessor.vala

@@ -0,0 +1,123 @@
+using Invercargill.DataStructures;
+using Invercargill.Wrappers;
+
+namespace Invercargill.Expressions {
+
+    /**
+     * Property accessor for string types.
+     * 
+     * Provides access to string properties:
+     * - `length`: The length of the string (int)
+     * - `empty`: Whether the string is empty (bool)
+     * - `is_null`: Whether the string is null (bool)
+     * - `is_empty_or_null`: Whether the string is empty or null (bool)
+     * 
+     * Example usage in expressions:
+     * {{{
+     * name.length          // Returns the length of name
+     * name.empty           // Returns true if name is ""
+     * name.is_null         // Returns true if name is null
+     * name.is_empty_or_null // Returns true if name is "" or null
+     * }}}
+     */
+    public class StringPropertyAccessor : Object, PropertyAccessor {
+
+        /**
+         * The string value being accessed.
+         */
+        private string? _value;
+
+        /**
+         * Creates a new StringPropertyAccessor.
+         * 
+         * @param value The string value to access properties on
+         */
+        public StringPropertyAccessor(string? value) {
+            _value = value;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public bool has_property(string property) {
+            return property == "length" ||
+                   property == "empty" ||
+                   property == "is_null" ||
+                   property == "is_empty_or_null";
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Enumerable<string> get_property_names() {
+            return new Wrappers.Array<string>(new string[]{"length", "empty", "is_null", "is_empty_or_null"});
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Element read_property(string property) throws ExpressionError {
+            switch (property) {
+                case "length":
+                    return new NativeElement<int?>(_value == null ? 0 : _value.length);
+                
+                case "empty":
+                    return new NativeElement<bool?>(_value != null && _value.length == 0);
+                
+                case "is_null":
+                    return new NativeElement<bool?>(_value == null);
+                
+                case "is_empty_or_null":
+                    return new NativeElement<bool?>(_value == null || _value.length == 0);
+                
+                default:
+                    throw new ExpressionError.NON_EXISTANT_PROPERTY(
+                        @"Unknown string property: $property"
+                    );
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Type get_property_type(string property) throws ExpressionError {
+            switch (property) {
+                case "length":
+                    return typeof(int?);
+                
+                case "empty":
+                case "is_null":
+                case "is_empty_or_null":
+                    return typeof(bool?);
+                
+                default:
+                    throw new ExpressionError.NON_EXISTANT_PROPERTY(
+                        @"Unknown string property: $property"
+                    );
+            }
+        }
+
+    }
+
+    /**
+     * Factory for creating StringPropertyAccessor instances.
+     * 
+     * This factory is registered with the TypeAccessorRegistry to provide
+     * property access for string values.
+     */
+    public class StringPropertyAccessorFactory : Object, PropertyAccessorFactory {
+
+        /**
+         * {@inheritDoc}
+         */
+        public PropertyAccessor? create(Element element) {
+            string? value;
+            if (element.try_get_as(out value)) {
+                return new StringPropertyAccessor(value);
+            }
+            return null;
+        }
+
+    }
+
+}

+ 207 - 0
src/lib/Expressions/TypeAccessorRegistry.vala

@@ -0,0 +1,207 @@
+using Invercargill.DataStructures;
+
+namespace Invercargill.Expressions {
+
+    /**
+     * Registry for type-specific property and function accessors.
+     * 
+     * This class manages the registration of PropertyAccessorFactory and
+     * FunctionAccessorFactory instances for specific types. When an expression
+     * needs to access a property or call a function on an Element, the registry
+     * is consulted to find an appropriate accessor.
+     * 
+     * The registry supports:
+     * - Registering factories for specific types
+     * - Fallback to default behavior (GObject properties, Enumerable functions)
+     * - Global singleton access for convenience
+     * 
+     * Example usage:
+     * {{{
+     * var registry = new TypeAccessorRegistry();
+     * registry.register_property_accessor(new StringPropertyAccessorFactory());
+     * registry.register_function_accessor(new StringFunctionAccessorFactory());
+     * 
+     * var accessor = registry.get_property_accessor(element);
+     * if (accessor != null && accessor.has_property("length")) {
+     *     var length = accessor.read_property("length");
+     * }
+     * }}}
+     */
+    public class TypeAccessorRegistry : Object {
+
+        /**
+         * Singleton instance for global access.
+         */
+        private static TypeAccessorRegistry? _instance = null;
+
+        /**
+         * Gets the global singleton instance, creating it if necessary.
+         * 
+         * @return The global TypeAccessorRegistry instance
+         */
+        public static TypeAccessorRegistry get_instance() {
+            if (_instance == null) {
+                _instance = new TypeAccessorRegistry();
+                _instance.register_defaults();
+            }
+            return _instance;
+        }
+
+        /**
+         * Resets the singleton instance (useful for testing).
+         */
+        public static void reset_instance() {
+            _instance = null;
+        }
+
+        /**
+         * Registered property accessor factories.
+         */
+        private GenericArray<PropertyAccessorFactory> _property_factories =
+            new GenericArray<PropertyAccessorFactory>();
+
+        /**
+         * Registered function accessor factories.
+         */
+        private GenericArray<FunctionAccessorFactory> _function_factories =
+            new GenericArray<FunctionAccessorFactory>();
+
+        /**
+         * Registers a property accessor factory.
+         * 
+         * Factories are consulted in registration order; the first factory
+         * that returns a non-null accessor is used.
+         * 
+         * @param factory The factory to register
+         */
+        public void register_property_accessor(PropertyAccessorFactory factory) {
+            _property_factories.add(factory);
+        }
+
+        /**
+         * Registers a function accessor factory.
+         * 
+         * Factories are consulted in registration order; the first factory
+         * that returns a non-null accessor is used.
+         * 
+         * @param factory The factory to register
+         */
+        public void register_function_accessor(FunctionAccessorFactory factory) {
+            _function_factories.add(factory);
+        }
+
+        /**
+         * Gets a property accessor for the given Element.
+         * 
+         * This method consults all registered factories in order until one
+         * returns a non-null accessor. If no registered factory handles the
+         * element, fallback behavior is used (GObject properties).
+         * 
+         * @param element The Element to get a property accessor for
+         * @return A PropertyAccessor if available, null otherwise
+         */
+        public PropertyAccessor? get_property_accessor(Element element) {
+            // Try registered factories first
+            foreach (var factory in _property_factories) {
+                var accessor = factory.create(element);
+                if (accessor != null) {
+                    return accessor;
+                }
+            }
+
+            // Fallback: Check if element directly implements PropertyAccessor
+            PropertyAccessor? accessor;
+            if (element.try_get_as(out accessor)) {
+                return accessor;
+            }
+
+            // Fallback: Use ObjectPropertyAccessor for Objects
+            Object? obj;
+            if (element.try_get_as(out obj)) {
+                return new ObjectPropertyAccessor(obj);
+            }
+
+            return null;
+        }
+
+        /**
+         * Gets a function accessor for the given Element.
+         * 
+         * This method consults all registered factories in order until one
+         * returns a non-null accessor. If no registered factory handles the
+         * element, fallback behavior is used (Enumerable functions).
+         * 
+         * @param element The Element to get a function accessor for
+         * @return A FunctionAccessor if available, null otherwise
+         */
+        public FunctionAccessor? get_function_accessor(Element element) {
+            // Check for GlobalFunctionsElement first (for global functions)
+            if (element is GlobalFunctionsElement) {
+                return ((GlobalFunctionsElement) element).accessor;
+            }
+
+            // Try registered factories
+            foreach (var factory in _function_factories) {
+                var accessor = factory.create(element);
+                if (accessor != null) {
+                    return accessor;
+                }
+            }
+
+            // Fallback: Check if element directly implements FunctionAccessor
+            FunctionAccessor? accessor;
+            if (element.try_get_as(out accessor)) {
+                return accessor;
+            }
+
+            // Fallback: Check for Elements (Enumerable wrapper)
+            Elements? elements;
+            if (element.try_get_as(out elements)) {
+                return new EnumerableFunctionAccessor(elements);
+            }
+
+            // Fallback: Check for Enumerable directly
+            Enumerable? enumerable;
+            if (element.try_get_as(out enumerable)) {
+                return new EnumerableFunctionAccessor(enumerable.to_elements());
+            }
+
+            return null;
+        }
+
+        /**
+         * Checks if a property accessor is available for the given Element.
+         * 
+         * @param element The Element to check
+         * @return true if a property accessor is available
+         */
+        public bool has_property_accessor(Element element) {
+            return get_property_accessor(element) != null;
+        }
+
+        /**
+         * Checks if a function accessor is available for the given Element.
+         * 
+         * @param element The Element to check
+         * @return true if a function accessor is available
+         */
+        public bool has_function_accessor(Element element) {
+            return get_function_accessor(element) != null;
+        }
+
+        /**
+         * Registers default accessor factories.
+         * 
+         * This is called automatically when the singleton is created.
+         * Currently registers:
+         * - StringPropertyAccessorFactory for string properties
+         * - StringFunctionAccessorFactory for string functions
+         */
+        private void register_defaults() {
+            register_property_accessor(new StringPropertyAccessorFactory());
+            register_function_accessor(new StringFunctionAccessorFactory());
+        }
+
+    }
+
+}

+ 4 - 0
src/lib/meson.build

@@ -156,6 +156,10 @@ sources += files('Expressions/IterateFunctionAccessor.vala')
 sources += files('Expressions/GlobalFunctionAccessor.vala')
 sources += files('Expressions/ExpressionTokenizer.vala')
 sources += files('Expressions/ExpressionParser.vala')
+sources += files('Expressions/AccessorFactory.vala')
+sources += files('Expressions/TypeAccessorRegistry.vala')
+sources += files('Expressions/StringPropertyAccessor.vala')
+sources += files('Expressions/StringFunctionAccessor.vala')
 
 sources += files('Expressions/Expressions/LiteralExpression.vala')
 sources += files('Expressions/Expressions/VariableExpression.vala')

+ 293 - 0
src/tests/Integration/Expressions.vala

@@ -1442,4 +1442,297 @@ void expression_tests() {
         assert(result.try_get_as<bool?>(out value));
         assert(value == true); // Bob has rank 5 > min_rank 2, and is_important = true
     });
+
+    // ==================== String Property Accessor Tests ====================
+
+    Test.add_func("/invercargill/expressions/string_length", () => {
+        var expr = ExpressionParser.parse("s.length");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        int? value;
+        assert(result.try_get_as<int?>(out value));
+        assert(value == 5);
+    });
+
+    Test.add_func("/invercargill/expressions/string_empty", () => {
+        var expr = ExpressionParser.parse("s.empty");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>(""));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/string_is_null", () => {
+        var expr = ExpressionParser.parse("s.is_null");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>(null));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/string_is_empty_or_null", () => {
+        // Test with empty string
+        var expr = ExpressionParser.parse("s.is_empty_or_null");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>(""));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
+
+    // ==================== String Function Accessor Tests ====================
+
+    Test.add_func("/invercargill/expressions/string_chomp", () => {
+        var expr = ExpressionParser.parse("s.chomp()");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello  "));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "hello");
+    });
+
+    Test.add_func("/invercargill/expressions/string_chug", () => {
+        var expr = ExpressionParser.parse("s.chug()");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("  hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "hello");
+    });
+
+    Test.add_func("/invercargill/expressions/string_upper", () => {
+        var expr = ExpressionParser.parse("s.upper()");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "HELLO");
+    });
+
+    Test.add_func("/invercargill/expressions/string_lower", () => {
+        var expr = ExpressionParser.parse("s.lower()");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("HELLO"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "hello");
+    });
+
+    Test.add_func("/invercargill/expressions/string_contains", () => {
+        var expr = ExpressionParser.parse("s.contains(\"ell\")");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/string_has_prefix", () => {
+        var expr = ExpressionParser.parse("s.has_prefix(\"hel\")");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/string_has_suffix", () => {
+        var expr = ExpressionParser.parse("s.has_suffix(\"llo\")");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/string_replace", () => {
+        var expr = ExpressionParser.parse("s.replace(\"l\", \"x\")");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "hexxo");
+    });
+
+    Test.add_func("/invercargill/expressions/string_substring", () => {
+        var expr = ExpressionParser.parse("s.substring(1, 3)");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "ell");
+    });
+
+    Test.add_func("/invercargill/expressions/string_split", () => {
+        var expr = ExpressionParser.parse("s.split(\",\")");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("a,b,c"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        Enumerable<string?>? parts;
+        assert(result.try_get_as<Enumerable<string?>>(out parts));
+        assert(parts != null);
+        
+        var count = 0;
+        foreach (var part in parts) {
+            count++;
+        }
+        assert(count == 3);
+    });
+
+    Test.add_func("/invercargill/expressions/string_char_at", () => {
+        var expr = ExpressionParser.parse("s.char_at(1)");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "e");
+    });
+
+    Test.add_func("/invercargill/expressions/string_char_at_negative_index", () => {
+        // Test negative index (from end)
+        var expr = ExpressionParser.parse("s.char_at(-1)");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "o");
+    });
+
+    Test.add_func("/invercargill/expressions/string_char_at_out_of_bounds", () => {
+        // Test out of bounds returns null
+        var expr = ExpressionParser.parse("s.char_at(100)");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        assert(result.is_null());
+    });
+
+    Test.add_func("/invercargill/expressions/string_reverse", () => {
+        var expr = ExpressionParser.parse("s.reverse()");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("hello"));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "olleh");
+    });
+
+    Test.add_func("/invercargill/expressions/string_chained_operations", () => {
+        // Test chaining: s.chug().chomp().upper()
+        var expr = ExpressionParser.parse("s.chug().chomp().upper()");
+        var props = new PropertyDictionary();
+        props.set("s", new NativeElement<string?>("  hello  "));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        string? value;
+        assert(result.try_get_as<string?>(out value));
+        assert(value == "HELLO");
+    });
+
+    Test.add_func("/invercargill/expressions/string_in_complex_expression", () => {
+        // Test string operations in a complex expression
+        // name.chug().chomp().length > 3
+        var expr = ExpressionParser.parse("name.chug().chomp().length > 3");
+        var props = new PropertyDictionary();
+        props.set("name", new NativeElement<string?>("  hello  "));
+        var context = new EvaluationContext(props);
+        
+        var result = expr.evaluate(context);
+        assert(result != null);
+        
+        bool? value;
+        assert(result.try_get_as<bool?>(out value));
+        assert(value == true);
+    });
 }