From 3f850cc9b069c4e4f890e5deafd3327e5a2fbda0 Mon Sep 17 00:00:00 2001 From: Marc Hernandez Date: Wed, 28 May 2025 10:46:00 -0700 Subject: [PATCH] Implement Xml Serialization Framework with Type Handling and Metadata Caching - Introduced XmlSer class for XML serialization and deserialization. - Added TypeMetaCache for caching type metadata and reflection information. - Implemented various ITypeHandler implementations for handling different types (Primitive, Proxy, ISerializable, Collection, Object). - Enhanced type resolution with TypeResolver to manage type lookups and conversions. - Established a configuration class (XmlCfg) to manage serialization settings. - Integrated support for handling graphs and references in serialized objects. - Added extensive member processing and filtering based on attributes. - Ensured compatibility with immutable collections and various data structures. - Implemented detailed error handling and logging for serialization processes. --- .editorconfig | 551 +++++++++++ cfg/Config.cs | 4 +- imm/FSM.cs | 177 ++-- imm/Imm.cs | 663 ++++++------- imm/List.cs | 201 ++-- imm/iu.cs | 89 +- logging/Log.cs | 278 +++--- res/Resource.cs | 694 +++++++------ ...onary.cs => SerializableDictionary.cs_bad} | 0 ...onFormatter.cs => VersionFormatter.cs_bad} | 0 ser/{XmlFormatter.cs => XmlFormatter.cs_bad} | 2 +- ser/XmlFormatter2.cs | 7 +- ser/XmlSer.cs | 917 ++++++++++++++++++ 13 files changed, 2472 insertions(+), 1111 deletions(-) create mode 100644 .editorconfig rename ser/{SerializableDictionary.cs => SerializableDictionary.cs_bad} (100%) rename ser/{VersionFormatter.cs => VersionFormatter.cs_bad} (100%) rename ser/{XmlFormatter.cs => XmlFormatter.cs_bad} (99%) create mode 100644 ser/XmlSer.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fd356df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,551 @@ +# Version: 4.1.1 (Using https://semver.org/) +# Updated: 2022-05-23 +# See https://github.com/RehanSaeed/EditorConfig/releases for release notes. +# See https://github.com/RehanSaeed/EditorConfig for updates to this file. +# See http://EditorConfig.org for more information about .editorconfig files. + +########################################## +# Common Settings +########################################## + +# This file is the top-most EditorConfig file +root = true + +# All Files +[*] +charset = utf-8 +indent_style = tab +# indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +########################################## +# File Extension Settings +########################################## + +# Visual Studio Solution Files +[*.sln] +indent_style = tab + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.{md,mdx}] +trim_trailing_whitespace = false + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] +indent_size = 2 + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Bash Files +[*.sh] +end_of_line = lf + +# Makefiles +[Makefile] +indent_style = tab + +########################################## +# Default .NET Code Style Severities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope +########################################## + +[*.{cs,csx,cake,vb,vbx}] +# Default Severity for all .NET Code Style rules below +dotnet_analyzer_diagnostic.severity = none + +########################################## +# Language Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules +########################################## + +# .NET Style Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules +[*.{cs,csx,cake,vb,vbx}] +indent_style = tab + +# "this." and "Me." qualifiers +dotnet_style_qualification_for_field = false:none +dotnet_style_qualification_for_property = false:none +dotnet_style_qualification_for_method = false:none +dotnet_style_qualification_for_event = false:none +# Language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:none +dotnet_style_predefined_type_for_member_access = true:none +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:none +csharp_preferred_modifier_order = new,static,private,virtual,public,protected,internal,extern,abstract,sealed,override,readonly,unsafe,volatile,async:none +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:none +dotnet_style_readonly_field = true:none +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:none +# Expression-level preferences +dotnet_style_object_initializer = true:none +dotnet_style_collection_initializer = true:none +dotnet_style_explicit_tuple_names = true:none +dotnet_style_prefer_inferred_tuple_names = true:none +dotnet_style_prefer_inferred_anonymous_type_member_names = true:none +dotnet_style_prefer_auto_properties = true:none +dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion +dotnet_diagnostic.IDE0045.severity = suggestion +dotnet_style_prefer_conditional_expression_over_return = false:suggestion +dotnet_diagnostic.IDE0046.severity = suggestion +dotnet_style_prefer_compound_assignment = true:none +dotnet_style_prefer_simplified_interpolation = true:none +dotnet_style_prefer_simplified_boolean_expressions = true:none +# Null-checking preferences +dotnet_style_coalesce_expression = true:none +dotnet_style_null_propagation = true:none +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:none +# File header preferences +# file_header_template = \n© PROJECT-AUTHOR\n +# If you use StyleCop, you'll need to disable SA1636: File header copyright text should match. +# dotnet_diagnostic.SA1636.severity = none +# Undocumented +dotnet_style_operator_placement_when_wrapping = end_of_line:none +csharp_style_prefer_null_check_over_type_check = true:none + +# C# Style Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules +[*.{cs,csx,cake}] +# 'var' preferences +csharp_style_var_for_built_in_types = true:none +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_elsewhere = false +# Expression-bodied members +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = true:none +csharp_style_expression_bodied_operators = true:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none +csharp_style_expression_bodied_lambdas = true:none +csharp_style_expression_bodied_local_functions = true:none +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:none +csharp_style_pattern_matching_over_as_with_null_check = true:none +csharp_style_prefer_switch_expression = true:none +csharp_style_prefer_pattern_matching = true:none +csharp_style_prefer_not_pattern = true:none +# Expression-level preferences +csharp_style_inlined_variable_declaration = true:none +csharp_prefer_simple_default_expression = true:none +csharp_style_pattern_local_over_anonymous_function = true:none +csharp_style_deconstructed_variable_declaration = true:none +csharp_style_prefer_index_operator = true:none +csharp_style_prefer_range_operator = true:none +csharp_style_implicit_object_creation_when_type_is_apparent = true:none +# "Null" checking preferences +csharp_style_throw_expression = true:none +csharp_style_conditional_delegate_call = true:none +# Code block preferences +csharp_prefer_braces = true:none +csharp_prefer_simple_using_statement = true:suggestion +dotnet_diagnostic.IDE0063.severity = suggestion +# 'using' directive preferences +# csharp_using_directive_placement = inside_namespace:none +# Modifier preferences +csharp_prefer_static_local_function = true:none + +########################################## +# Unnecessary Code Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules +########################################## + +# .NET Unnecessary code rules +[*.{cs,csx,cake,vb,vbx}] +dotnet_code_quality_unused_parameters = all:none +dotnet_remove_unnecessary_suppression_exclusions = none:none + +# C# Unnecessary code rules +[*.{cs,csx,cake}] +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion +dotnet_diagnostic.IDE0058.severity = suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +dotnet_diagnostic.IDE0059.severity = suggestion + +########################################## +# Formatting Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules +########################################## + +# .NET formatting rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#net-formatting-rules +[*.{cs,csx,cake,vb,vbx}] +# Organize using directives +dotnet_sort_system_directives_first = false +dotnet_separate_import_directive_groups = false +# Dotnet namespace options +dotnet_style_namespace_match_folder = true:suggestion +dotnet_diagnostic.IDE0130.severity = suggestion + +# C# formatting rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#c-formatting-rules +[*.{cs,csx,cake}] +# Newline options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#new-line-options +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#indentation-options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false +# Spacing options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#spacing-options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = false +csharp_space_between_parentheses = control_flow_statements, expressions +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = no_change +csharp_space_between_method_declaration_parameter_list_parentheses = true +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false +# Wrap options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#wrap-options +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true +# Namespace options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#namespace-options +csharp_style_namespace_declarations = file_scoped:none + +########################################## +# .NET Naming Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/naming-rules +########################################## + +[*.{cs,csx,cake,vb,vbx}] + +########################################## +# Styles +########################################## + +# camel_case_style - Define the camelCase style +dotnet_naming_style.camel_case_style.capitalization = camel_case +# pascal_case_style - Define the PascalCase style +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# first_upper_style - The first character must start with an upper-case character +dotnet_naming_style.first_upper_style.capitalization = first_word_upper +# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' +# dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case +# dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T +# disallowed_style - Anything that has this style applied is marked as disallowed +dotnet_naming_style.disallowed_style.capitalization = pascal_case +dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ +dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ +# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file +dotnet_naming_style.internal_error_style.capitalization = pascal_case +dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ +dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ + + +########################################## +# .NET Design Guideline Field Naming Rules +# Naming rules for fields follow the .NET Framework design guidelines +# https://docs.microsoft.com/dotnet/standard/design-guidelines/index +########################################## + +# All public/protected/protected_internal constant fields must be PascalCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = none + +# All public/protected/protected_internal static readonly fields must be PascalCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = none + +# No other public/protected/protected_internal fields are allowed +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +#dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal +#dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field +#dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group +#dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style +#dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error + +########################################## +# StyleCop Field Naming Rules +# Naming rules for fields follow the StyleCop analyzers +# This does not override any rules using disallowed_style above +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers +########################################## + +# All constant fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = prefix_underscore +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = none + +# All static readonly fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = prefix_underscore +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = none + +# No non-private instance fields are allowed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error + +dotnet_naming_symbols.stylecop_all_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +#dotnet_naming_symbols.stylecop_all_fields_group.applicable_accessibilities = * +dotnet_naming_symbols.stylecop_all_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_all_fields_group +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = prefix_underscore +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = none + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + + +# Private fields must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md + +dotnet_naming_symbols.stylecop_static_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_static_fields_group.required_modifiers = static +dotnet_naming_symbols.stylecop_static_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_static_fields_group +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = prefix_s_underscore +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = none + + +dotnet_naming_style.prefix_s_underscore.capitalization = camel_case +dotnet_naming_style.prefix_s_underscore.required_prefix = s_ + + +# Local variables must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent + + +d + + +# This rule should never fire. However, it's included for at least two purposes: +# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. +# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). +#dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * +#dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field +#dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group +#dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style +#dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error + + +########################################## +# Other Naming Rules +########################################## + +# All of the following must be PascalCase: +# - Namespaces +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Classes and Enumerations +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Delegates +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types +# - Constructors, Properties, Events, Methods +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_rule.element_rule.style = pascal_case_style +dotnet_naming_rule.element_rule.severity = none + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.interface_group.applicable_kinds = interface +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_rule.interface_rule.style = camel_case +dotnet_naming_rule.interface_rule.severity = none + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.severity = none + +# Function parameters use camelCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.severity = none + + + +########################################## +# Chickensoft Rule Overrides +########################################## + +# Allow using keywords as names +# dotnet_diagnostic.CA1716.severity = none +# Don't require culture info for ToString() +dotnet_diagnostic.CA1304.severity = none +# Don't require a string comparison for comparing strings. +dotnet_diagnostic.CA1310.severity = none +# Don't require a string format specifier. +dotnet_diagnostic.CA1305.severity = none +# Allow protected fields. +dotnet_diagnostic.CA1051.severity = none +# Don't warn about checking values that are supposedly never null. Sometimes +# they are actually null. +dotnet_diagnostic.CS8073.severity = none +# Don't remove seemingly "unnecessary" assignments, as they often have +# intended side-effects. +dotnet_diagnostic.IDE0059.severity = none +# Switch/case should always have a default clause. Tell that to Roslynator. +dotnet_diagnostic.RCS1070.severity = none +# Tell roslynator not to eat unused parameters. +dotnet_diagnostic.RCS1163.severity = none +# Tell dotnet not to remove unused parameters. +dotnet_diagnostic.IDE0060.severity = none +# Tell roslynator not to remove `partial` modifiers. +dotnet_diagnostic.RCS1043.severity = none +# Tell roslynator not to make classes static so aggressively. +dotnet_diagnostic.RCS1102.severity = none +# Roslynator wants to make properties readonly all the time, so stop it. +# The developer knows best when it comes to contract definitions with Godot. +dotnet_diagnostic.RCS1170.severity = none +# Allow expression values to go unused, even without discard variable. +# Otherwise, using Moq would be way too verbose. +dotnet_diagnostic.IDE0058.severity = none +# Don't let roslynator turn every local variable into a const. +# If we did, we'd have to specify the types of local variables far more often, +# and this style prefers type inference. +dotnet_diagnostic.RCS1118.severity = none +# Enums don't need to declare explicit values. Everyone knows they start at 0. +dotnet_diagnostic.RCS1161.severity = none +# Allow unconstrained type parameter to be checked for null. +dotnet_diagnostic.RCS1165.severity = none +# Allow keyword-based names so that parameter names like `@event` can be used. +dotnet_diagnostic.CA1716.severity = none +# Allow me to use the word Collection if I want. +dotnet_diagnostic.CA1711.severity = none +# Not disposing of objects in a test is normal within Godot because of scene tree stuff. +dotnet_diagnostic.CA1001.severity = none +# No primary constructors — not supported well by tooling. +dotnet_diagnostic.IDE0290.severity = none +# Let me comment where I like +dotnet_diagnostic.RCS1181.severity = none +# Let me write dumb if checks, keeps it readable +dotnet_diagnostic.IDE0046.severity = none +# Don't make me use expression bodies for methods +dotnet_diagnostic.IDE0022.severity = none +# Don't use collection shorhand. +dotnet_diagnostic.IDE0300.severity = none +dotnet_diagnostic.IDE0028.severity = none +dotnet_diagnostic.IDE0305.severity = none +# Don't make me populate a switch expression redundantly +dotnet_diagnostic.IDE0072.severity = none + + + + +########################################## +# License +########################################## +# The following applies as to the .editorconfig file ONLY, and is +# included below for reference, per the requirements of the license +# corresponding to this .editorconfig file. +# See: https://github.com/RehanSaeed/EditorConfig +# +# MIT License +# +# Copyright (c) 2017-2019 Muhammad Rehan Saeed +# Copyright (c) 2019 Henry Gabryjelski +# +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject +# to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +########################################## + +# CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language. +dotnet_diagnostic.CS8981.severity = silent + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = silent diff --git a/cfg/Config.cs b/cfg/Config.cs index 1f83864..7ca7fdc 100644 --- a/cfg/Config.cs +++ b/cfg/Config.cs @@ -54,8 +54,8 @@ namespace lib static public void startup( string filename ) { - res.Mgr.register( load ); - res.Mgr.registerSub( typeof( ConfigBase ) ); + res.Mgr.Register( load ); + res.Mgr.RegisterSub( typeof( ConfigBase ) ); s_cfg = load( filename ); diff --git a/imm/FSM.cs b/imm/FSM.cs index 0afd7d5..3b5991b 100644 --- a/imm/FSM.cs +++ b/imm/FSM.cs @@ -1,103 +1,110 @@ - +#nullable enable using System; using System.Runtime.CompilerServices; -using Optional; +using imm; // Ensure this namespace is available - - -namespace imm; - - - -public record class Context : imm.Recorded, Imm +/// +/// Base context for an FSM. +/// MUST inherit from Recorded or Timed in your concrete class. +/// +/// The concrete Context type. +public abstract record class FsmContextBase : Recorded + where TSelf : FsmContextBase { - Meta Imm.Meta => base.Meta; + // Required for 'with' expressions. + protected FsmContextBase(Recorded original) : base(original) { } + protected FsmContextBase() { } } -public record class State( CTX Context ) : imm.Recorded, Imm - where TSUB : State - where CTX : Context +/// +/// Base state for an FSM. +/// MUST inherit from Recorded or Timed in your concrete class. +/// +/// The concrete State type. +/// The concrete Context type (must be based on FsmContextBase). +public abstract record class FsmStateBase : Recorded + where TSelf : FsmStateBase + where TCtx : FsmContextBase { - Meta Imm.Meta => base.Meta; + /// + /// Called when entering this state. + /// + public virtual (TCtx Context, TSelf State) OnEnter(TCtx context, FsmStateBase oldState) + { + return (context, (TSelf)this); + } - virtual public (CTX, TSUB) onEnter(CTX ctx, State oldState) - { - return (ctx, (TSUB)this); - } + /// + /// Called when exiting this state. + /// + public virtual (TCtx Context, TSelf State) OnExit(TCtx context, FsmStateBase newState) + { + return (context, (TSelf)this); + } - virtual public (CTX, TSUB) onExit(CTX ctx, State newState) - { - return (ctx, (TSUB)this); - } + // Required for 'with' expressions. + protected FsmStateBase(Recorded original) : base(original) { } + protected FsmStateBase() { } } - - -public record class FSM : imm.Recorded, Imm - where TSUB : FSM - where ST : State - where CTX : Context +/// +/// An immutable FSM base class. +/// MUST inherit from Recorded or Timed in your concrete class. +/// +/// The concrete FSM type. +/// The concrete State type. +/// The concrete Context type. +public abstract record class FsmBase : Recorded + where TSelf : FsmBase + where TState : FsmStateBase + where TCtx : FsmContextBase { - Meta Imm.Meta => base.Meta; + public TCtx Context { get; init; } + public TState State { get; init; } - public CTX Context { get; init; } - public ST State { get; init; } + protected FsmBase(TCtx initialContext, TState initialState) + { + Context = initialContext; + State = initialState; + } - public FSM( CTX context, ST stStart ) - { - Context = context; - State = stStart; - } + // Required for 'with' expressions. + protected FsmBase(Recorded original) : base(original) + { + var o = original as FsmBase; + Context = o!.Context; + State = o!.State; + } - public TSUB Transition(ST newState, string reason, - [CallerMemberName] string memberName = "", - [CallerFilePath] string filePath = "", - [CallerLineNumber] int lineNumber = 0, - [CallerArgumentExpression("newState")] - string expression = default - ) - { - log.debug( $"Trans from {State.GetType().Name} to {newState.GetType().Name} for {reason}" ); + /// + /// Transitions the FSM. It automatically uses the 'Process' + /// method appropriate for Recorded or Timed, thanks to virtual overrides. + /// + public TSelf Transition( + TState newState, + string reason, + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0, + [CallerArgumentExpression("newState")] string expression = "") + { + Console.WriteLine($"[FSM] Transition: {State.GetType().Name} -> {newState.GetType().Name}. Reason: {reason}"); - var origState = State; - - var (newCtx, oldState) = State.onExit(Context, newState); - - var (newCTX, storeState) = newState.onEnter(newCtx, oldState); - - var newFSM = this.Process( (v) => (this as TSUB) with - { - Context = newCTX, - State = storeState, - }, $"{reason}" ); - - return newFSM; - } - - /* - public TSUB ( Func fn, string reason, - [CallerMemberName] string member = "", - [CallerFilePath] string file = "", - [CallerLineNumber] int line = 0, - [CallerArgumentExpression("fn")] - string expression = default - ) - { - var newState = fn( State ); - - if( object.ReferenceEquals( newState, State ) ) return (TSUB)this; - - TSUB newFSM = this.Process( this with - { - Context = Context, - State = newState, - }, $"Processing: {newState.GetType().Name} for {reason}", - member, file, line, expression ); - - return (TSUB)newFSM; - } - */ - -} + var (ctxAfterExit, stateAfterExit) = State.OnExit(Context, newState); + var (ctxAfterEnter, stateAfterEnter) = newState.OnEnter(ctxAfterExit, stateAfterExit); + // Since 'this' is at least 'Recorded', we can call the + // detailed 'Process'. If 'this' is actually 'Timed', C#'s + // virtual dispatch will call the 'Timed' override automatically. + return Process( + fsm => (TSelf)fsm with + { + Context = ctxAfterEnter, + State = stateAfterEnter + }, + $"Transition to {newState.GetType().Name}: {reason}", + memberName, filePath, lineNumber, expression + ); + } +} \ No newline at end of file diff --git a/imm/Imm.cs b/imm/Imm.cs index 5985c9f..d20a359 100644 --- a/imm/Imm.cs +++ b/imm/Imm.cs @@ -1,404 +1,337 @@ - - -#nullable enable - +#nullable enable using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.Serialization; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using lib; namespace imm; +/// +/// Represents the base interface for versioned, immutable objects. +/// Provides access to metadata and potentially the previous version. +/// +public interface Obj +{ + /// + /// Gets the base metadata associated with this version. + /// + Metadata_Versioned Meta { get; } + + /// + /// Gets the previous version as a base object, if available. + /// Returns null if this is the first version or if history is not tracked. + /// + Obj? Old { get; } + + /// + /// Creates a new version without functional change. + /// Returns the new version as an Obj. + /// + Obj Record( + string reason = "Recorded", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0 ); +} + + +/// +/// Obj delegate for change notifications. +/// +public delegate void ChangeDelegate( T? oldVersion, T newVersion ); + + +/// +/// Represents a generic interface for immutable objects, +/// providing access to basic processing functions and change notifications. +/// +public interface Obj : Obj where T : Obj +{ + /// + /// Gets the change delegate associated with this object. + /// + ChangeDelegate OnChange { get; set; } + + /// + /// Applies a transformation and creates a new version using basic processing. + /// + T Process( + Func fn, + string reason = "Processed", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0 , + [CallerArgumentExpression("fn")] string expStr = "" ); + + + /// + /// Creates a new version without a functional change using basic processing. + /// Uses 'new' to provide a type-safe return. + /// + new T Record( + string reason = "Recorded", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0 ); +} + + /* -T O D O : -T O D O : -T O D O : -x) Add unit tests for all this. This will definitely benefit from them +static public class ObjExtensions +{ + /// + /// Creates a new version of the object with the specified reason. + /// + public static T Record( this T obj, string reason = "Recorded" ) where T : Obj + { + if( obj is Recorded recorded ) + { + return recorded.Record( reason ); + } + else if( obj is Versioned versioned ) + { + return versioned.Record( reason ); + } + else + { + // Dont care + + return obj; + } + } +} */ -static public class Util +// --- Metadata Hierarchy --- + +public interface VersionedMeta { - //This can handle both Timed and Recorded - static public T Process( ref T obj, Func fn, string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExp = "" ) - where T : Recorded - { - obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExp ); - return obj; - } - - static public T LightProcess( ref T obj, Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExp = "" - ) - where T : Versioned - { - obj = obj.Process( fn, reason ); - return obj; - } -} - -public interface Meta -{ - public uint Version => 0; - public string Reason => ""; - public string Expression => ""; - public string MemberName => ""; - public string FilePath => ""; - public int LineNumber => -1; - public DateTime CreatedAt => DateTime.MinValue; - public DateTime TouchedAt => DateTime.MinValue; - -} - -public interface Imm -{ - public Meta Meta { get; } - public object Record( string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ); - - public Imm Process( Imm next, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("next")] - string dbgExp = "" - ) - { - return next; - } - + public uint Version { get; } + public string Reason { get; } } -//[lib.Ser( Types = lib.Types.None )] -public record class Versioned : Imm - where T : Versioned +/// +/// Obj metadata for version tracking. +/// +public record Metadata_Versioned { - - public delegate void ChangeDelegate( T? old, T next ); - - public record class MetaData : Meta - { - public uint Version { get; internal set; } = 0; - public string Reason { get; internal set; } = ""; - - public MetaData() { } - } - - protected Versioned( ) - : this( new MetaData { Version = 1, Reason = $"Versioned.cons" } ) - { - } - - internal Versioned( MetaData meta ) - { - MetaStorage = meta; - } - - virtual public T Record( - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - { - return Process( t => t, reason ); - } - - - - protected MetaData MetaStorage = new(); - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public MetaData Meta => MetaStorage; - - Meta Imm.Meta => MetaStorage; - - [lib.Dont] - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public ChangeDelegate OnChange = (T? old,T cur) => {}; - - /* - public void AddOnChange( ChangeDelegate fn, - string reason, - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0` - ) - { - log.debug( $"ADD {log.whatFile(dbgPath)}({dbgLine}): {dbgName} added OnChange bcs {reason}" ); - OnChange += fn; - } - - public void RemOnChange( ChangeDelegate fn, - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - { - log.debug( $"REM {log.whatFile(dbgPath)}({dbgLine}): {dbgName} removing OnChange" ); - OnChange -= fn; - } - */ - - - public T Process( Func fn, string reason = "" ) - { - var newT = fn( ( T )this ); - - return newT with - { - MetaStorage = Meta with - { - Version = newT.Meta.Version + 1, - Reason = reason, - } - }; - } - - object Imm.Record( string reason, string dbgName, string dbgPath, int dbgLine ) => Record( reason, dbgName, dbgPath, dbgLine ); - - //public object Record( string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) => Recorded( ); + public uint Version { get; init; } = 1; + public string Reason { get; init; } = "Created"; } -//[lib.Ser( Types = lib.Types.None )] -public record class Recorded : Versioned, imm.Imm - where T : Recorded + +public interface RecordedMeta : VersionedMeta { - - new public record class MetaData : Versioned.MetaData - { - [lib.Dont] - public T? Old_backing { get; internal set; } - public T? Old => Old_backing; - public string DbgName { get; internal set; } = ""; - public string DbgPath { get; internal set; } = ""; - public int DbgLine { get; internal set; } = -1; - public string DbgExp { get; internal set; } = ""; - - public MetaData() { } - } - - public Recorded() : this( new MetaData() ) - { - } - - public Recorded(MetaData meta) : base( meta ) - { - } - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - new public MetaData Meta => MetaStorage as MetaData ?? new MetaData(); - - - override public T Record( - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - { - return Process( t => t, reason, dbgName, dbgPath, dbgLine ); - } - - virtual public T Process( T next, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("next")] - string dbgExp = "" - ) - { - return ProcessWork( ( old ) => next, reason, dbgName, dbgPath, dbgLine, dbgExp ); - } - - - virtual public T Process( Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExp = "" - ) - { - return ProcessWork( fn, reason, dbgName, dbgPath, dbgLine, dbgExp ); - } - - virtual public T ProcessWork( Func fn, - string reason, - string dbgName, - string dbgPath, - int dbgLine, - string dbgExp - ) - { - var orig = ( T )this; - - var next = fn( orig ); - - if( object.ReferenceEquals( orig, next) ) - return next; - - var ret = next with - { - //Do the Versioned code here - MetaStorage = Meta with - { - Version = orig.Meta.Version + 1, - Reason = !string.IsNullOrWhiteSpace( reason ) ? reason : next.Meta.Reason, - - Old_backing = orig, - DbgName = dbgName, - DbgPath = dbgPath, - DbgLine = dbgLine, - DbgExp = dbgExp - } - }; - - OnChange( orig, ret ); - - return ret; - } + public string MemberName { get; } + public string FilePath { get; } + public int LineNumber { get; } + public string Expression { get; } } -public record class Timed : Recorded, imm.Imm - where T : Timed + +/// +/// Metadata for version and recording (debug/caller info, history). +/// +public record Metadata_Recorded : Metadata_Versioned, RecordedMeta { + internal object? OldObject { get; init; } = null; + public string MemberName { get; init; } = ""; + public string FilePath { get; init; } = ""; + public int LineNumber { get; init; } = 0; + public string Expression { get; init; } = ""; +} - new public record class MetaData : Recorded.MetaData - { - public readonly DateTime CreatedAt = DateTime.Now; - public DateTime TouchedAt { get; internal set; } = DateTime.Now; - } +public interface TimedMeta : RecordedMeta +{ + public DateTime CreatedAt { get; } + public DateTime TouchedAt { get; } +} - public Timed() : this( new MetaData() ) - { - } - public Timed( MetaData meta ) : base( meta ) - { - } +/// +/// Metadata for version, recording, and timing. +/// +public record Metadata_Timed : Metadata_Recorded, TimedMeta +{ + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + public DateTime TouchedAt { get; init; } = DateTime.UtcNow; +} - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - new public MetaData Meta => MetaStorage as MetaData ?? new MetaData(); +// --- Record Hierarchy --- - public TimeSpan Since => Meta.TouchedAt - Meta.Old?.Meta.TouchedAt ?? TimeSpan.MaxValue; +/// +/// Level 1: Basic versioning. Implements Obj. +/// +public record class Versioned : Obj where T : Versioned +{ + public Metadata_Versioned Meta { get; init; } = new(); - public void CallOnChange() - { - OnChange( Meta.Old, (T)this ); - } + [DebuggerBrowsable( DebuggerBrowsableState.Never )] + public ChangeDelegate OnChange { get; set; } = ( o, n ) => { }; - override public T Record( - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - { - return Process( t => t with { MetaStorage = t.Meta with { Reason = $"Record {reason}" }}, reason, dbgName, dbgPath, dbgLine ); - } + public virtual Obj? Old => null; - override public T Process( T next, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("next")] - string dbgExp = "" - ) - { - return ProcessWork( ( old ) => next, reason, dbgName, dbgPath, dbgLine, dbgExp ); - } + Metadata_Versioned Obj.Meta => this.Meta; + Obj? Obj.Old => this.Old; - public U ProcessFn( Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExp = "" - ) - where U : T - { - return (U)ProcessWork( fn as Func, reason, dbgName, dbgPath, dbgLine, dbgExp ); - } + public Versioned() { } + protected Versioned( Versioned original ) + { + OnChange = original.OnChange; + Meta = original.Meta; + } - override public T Process( Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExp = "" - ) - => ProcessWork( fn, reason, dbgName, dbgPath, dbgLine, dbgExp ); + public virtual T Process( + Func fn, + string reason = "Processed", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0 , + [CallerArgumentExpression("fn")] string expStr = "" ) + { + var current = (T)this; + var next = fn( current ); - override public T ProcessWork( Func fn, - string reason, - string dbgName, - string dbgPath, - int dbgLine, - string dbgExp - ) - { - var orig = ( T )this; + if( ReferenceEquals( current, next ) ) + return current; - var next = fn( orig ); + var newVersion = next with + { + Meta = new Metadata_Versioned { /*...*/ }, + OnChange = current.OnChange + }; + newVersion.OnChange( current, newVersion ); + return newVersion; + } - if( object.ReferenceEquals( orig, next) ) - return next; + /// + /// Basic Record. Made virtual. Implements Obj.Record. + /// + public virtual T Record( + string reason = "Recorded", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0 ) => Process( t => t, reason, memberName, filePath, lineNumber ); - var ret = next with - { - MetaStorage = Meta with - { - //Versioned - Version = orig.Meta.Version + 1, - Reason = !string.IsNullOrWhiteSpace( reason ) ? reason : next.Meta.Reason, - - //Recorded - DbgName = dbgName, - DbgPath = dbgPath, - DbgLine = dbgLine, - DbgExp = dbgExp, - Old_backing = orig, - - //Timed - TouchedAt = DateTime.Now, - } - - }; - - if( OnChange != null) - OnChange( orig, ret ); - - return ret; - } + /// + /// Implements Obj.Record by calling the virtual T Record. + /// + Obj Obj.Record( + string reason = "Recorded", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0 ) => this.Record( reason, memberName, filePath, lineNumber ); } + +/// +/// Level 2: Adds history and caller info. +/// +public record class Recorded : Versioned where T : Recorded +{ + new public Metadata_Recorded Meta { get; init; } = new(); + new public T? Old => Meta.OldObject as T; + + //public override Obj? Old => this.Old; + //Metadata_Versioned Obj.Meta => this.Meta; + + public Recorded() { } + protected Recorded(Recorded original) : base(original) { Meta = original.Meta; } + + public virtual T Process( + Func fn, + string reason = "", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0, + [CallerArgumentExpression("fn")] string expStr = "") + { + var current = (T)this; + var next = fn(current); + + if (ReferenceEquals(current, next)) return current; + + var newMeta = new Metadata_Recorded + { + Version = current.Meta.Version + 1, + Reason = reason, + MemberName = memberName, + FilePath = filePath, + LineNumber = lineNumber, + Expression = expStr, + OldObject = current + }; + + var newVersion = next with { Meta = newMeta, OnChange = current.OnChange }; + newVersion.OnChange(current, newVersion); + return newVersion; + } + + public new T Record( + string reason = "Recorded", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + return Process(t => t, reason, memberName, filePath, lineNumber ); + } + +} + +/// +/// Level 3: Adds timestamps. +/// +public record class Timed : Recorded where T : Timed +{ + new public Metadata_Timed Meta { get; init; } = new(); + //Metadata_Versioned Obj.Meta => this.Meta; + public TimeSpan SinceLastTouch => Meta.TouchedAt - (Old?.Meta as Metadata_Timed)?.TouchedAt ?? TimeSpan.Zero; + public TimeSpan TotalAge => Meta.TouchedAt - Meta.CreatedAt; + + public Timed() { } + protected Timed(Timed original) : base(original) { Meta = original.Meta; } + + public override T Process( + Func fn, + string reason = "", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0, + [CallerArgumentExpression("fn")] string expression = "") + { + var current = (T)this; + var next = fn(current); + + if (ReferenceEquals(current, next)) return current; + + var newMeta = new Metadata_Timed + { + Version = current.Meta.Version + 1, + Reason = reason, + MemberName = memberName, + FilePath = filePath, + LineNumber = lineNumber, + Expression = expression, + OldObject = current, + CreatedAt = DateTime.UtcNow, + TouchedAt = DateTime.UtcNow + }; + + var currentTimedMeta = current.Meta; + var newVersion = next with { Meta = newMeta, OnChange = current.OnChange }; + newVersion.OnChange(current, newVersion); + return newVersion; + } + + public new T Record( + string reason = "Recorded", + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + return Process(t => t, reason, memberName, filePath, lineNumber ); + } +} diff --git a/imm/List.cs b/imm/List.cs index d789711..5407809 100644 --- a/imm/List.cs +++ b/imm/List.cs @@ -1,158 +1,77 @@ -using System; +#nullable enable + +using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; +using System.Runtime.CompilerServices; namespace imm; +/// +/// An immutable list implementation that tracks history, metadata, and time. +/// public record class List : Timed>, IImmutableList { + static public List Empty { get; } = new(); - ImmutableList Values = ImmutableList.Empty; + public ImmutableList Values { get; init; } = ImmutableList.Empty; + public List() { } + // Required for 'with' expressions to work with the base class hierarchy + protected List(Timed> original) : base(original) { } + // Helper to apply changes using the Process method + private List Change( + Func, ImmutableList> listChange, + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0, + [CallerArgumentExpression("listChange")] string reason = "") + { + var newValues = listChange(Values); + return ReferenceEquals(Values, newValues) + ? this + : Process(l => l with { Values = newValues }, reason, memberName, filePath, lineNumber, reason); + } - public T this[int index] => (( IReadOnlyList )Values)[index]; - - public int Count => Values.Count; - - public List Add( T value ) - { - return this with { Values = Values.Add( value ) }; - } - - public List AddRange( IEnumerable items ) - { - return this with { Values = Values.AddRange( items ) }; - } - - public List Clear() - { - return this with { Values = Values.Clear() }; - } - - public IEnumerator GetEnumerator() - { - return Values.GetEnumerator(); - } - - public int IndexOf( T item, int index, int count, IEqualityComparer equalityComparer ) - { - return Values.IndexOf( item, index, count, equalityComparer ); - } - - public int IndexOf( T item ) - { - return Values.IndexOf( item, 0, Count, EqualityComparer.Default ); - } - - public List Insert( int index, T element ) - { - return this with { Values = Values.Insert( index, element ) }; - } - - public List InsertRange( int index, IEnumerable items ) - { - return this with { Values = Values.InsertRange( index, items ) }; - } - - public int LastIndexOf( T item, int index, int count, IEqualityComparer equalityComparer ) - { - return Values.LastIndexOf( item, index, count, equalityComparer ); - } - - - - public List Remove( T value ) - { - return Remove( value, EqualityComparer.Default ); - } - - public List Remove( T value, IEqualityComparer equalityComparer ) - { - return this with { Values = Values.Remove( value, equalityComparer ) }; - } - - public List RemoveAll( Predicate match ) - { - return this with { Values = Values.RemoveAll( match ) }; - } - - public List RemoveAt( int index ) - { - return this with { Values = Values.RemoveAt( index ) }; - } - - public List RemoveRange( IEnumerable items, IEqualityComparer equalityComparer ) - { - return this with { Values = Values.RemoveRange( items, equalityComparer ) }; - } - - public List RemoveRange( int index, int count ) - { - return this with { Values = Values.RemoveRange( index, count ) }; - } - - public List Replace( T oldValue, T newValue, IEqualityComparer equalityComparer ) - { - return this with { Values = Values.Replace( oldValue, newValue, equalityComparer ) }; - } - - public List SetItem( int index, T value ) - { - return this with { Values = Values.SetItem( index, value ) }; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return (( IEnumerable )Values).GetEnumerator(); - } - - IImmutableList IImmutableList.Clear() - { - return Clear(); - } - - IImmutableList IImmutableList.Add( T value ) - { - return Add( value ); - } - - IImmutableList IImmutableList.AddRange( IEnumerable items ) - { - return AddRange( items ); - } - - IImmutableList IImmutableList.Insert( int index, T element ) - { - return Insert( index, element ); - } - - IImmutableList IImmutableList.InsertRange( int index, IEnumerable items ) - { - return InsertRange( index, items ); - } - - IImmutableList IImmutableList.Remove( T value, IEqualityComparer equalityComparer ) - { - return Remove( value, equalityComparer ); - } - - IImmutableList IImmutableList.RemoveAll( Predicate match ) - { - return RemoveAll( match ); - } + // --- IImmutableList implementation using the Change helper --- + public T this[int index] => Values[index]; + public int Count => Values.Count; + public List Add(T value) => Change(v => v.Add(value)); + public List AddRange(IEnumerable items) => Change(v => v.AddRange(items)); + public List Clear() => Change(v => v.Clear()); + // ... Implement all other IImmutableList methods similarly ... + #region IImmutableList Implementation + public List Insert( int index, T element ) => Change( v => v.Insert( index, element ) ); + public List InsertRange( int index, IEnumerable items ) => Change( v => v.InsertRange( index, items ) ); + public List Remove( T value, IEqualityComparer? equalityComparer ) => Change( v => v.Remove( value, equalityComparer ) ); + public List Remove( T value ) => Remove( value, EqualityComparer.Default ); + public List RemoveAll( Predicate match ) => Change( v => v.RemoveAll( match ) ); + public List RemoveAt( int index ) => Change( v => v.RemoveAt( index ) ); + public List RemoveRange( IEnumerable items, IEqualityComparer? equalityComparer ) => Change( v => v.RemoveRange( items, equalityComparer ) ); + public List RemoveRange( int index, int count ) => Change( v => v.RemoveRange( index, count ) ); + public List Replace( T oldValue, T newValue, IEqualityComparer? equalityComparer ) => Change( v => v.Replace( oldValue, newValue, equalityComparer ) ); + public List SetItem( int index, T value ) => Change( v => v.SetItem( index, value ) ); + public int IndexOf( T item, int index, int count, IEqualityComparer? equalityComparer ) => Values.IndexOf( item, index, count, equalityComparer ?? EqualityComparer.Default ); + public int IndexOf( T item ) => IndexOf( item, 0, Count, EqualityComparer.Default ); + public int LastIndexOf( T item, int index, int count, IEqualityComparer? equalityComparer ) => Values.LastIndexOf( item, index, count, equalityComparer ?? EqualityComparer.Default ); + IImmutableList IImmutableList.Clear() => Clear(); + IImmutableList IImmutableList.Add( T value ) => Add( value ); + IImmutableList IImmutableList.AddRange( IEnumerable items ) => AddRange( items ); + IImmutableList IImmutableList.Insert( int index, T element ) => Insert( index, element ); + IImmutableList IImmutableList.InsertRange( int index, IEnumerable items ) => InsertRange( index, items ); + IImmutableList IImmutableList.Remove( T value, IEqualityComparer? equalityComparer ) => Remove( value, equalityComparer ); + IImmutableList IImmutableList.RemoveAll( Predicate match ) => RemoveAll( match ); IImmutableList IImmutableList.RemoveAt( int index ) => RemoveAt( index ); - - IImmutableList IImmutableList.RemoveRange( IEnumerable items, IEqualityComparer equalityComparer ) - => RemoveRange( items, equalityComparer ); - + IImmutableList IImmutableList.RemoveRange( IEnumerable items, IEqualityComparer? equalityComparer ) => RemoveRange( items, equalityComparer ); IImmutableList IImmutableList.RemoveRange( int index, int count ) => RemoveRange( index, count ); - - IImmutableList IImmutableList.Replace( T oldValue, T newValue, IEqualityComparer equalityComparer ) - => Replace( oldValue, newValue, equalityComparer ); - + IImmutableList IImmutableList.Replace( T oldValue, T newValue, IEqualityComparer? equalityComparer ) => Replace( oldValue, newValue, equalityComparer ); IImmutableList IImmutableList.SetItem( int index, T value ) => SetItem( index, value ); -} + #endregion + // --- Standard Interfaces --- + public IEnumerator GetEnumerator() => Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Values).GetEnumerator(); +} \ No newline at end of file diff --git a/imm/iu.cs b/imm/iu.cs index d048dfe..c1654b9 100644 --- a/imm/iu.cs +++ b/imm/iu.cs @@ -1,56 +1,47 @@ - -// A spot for immutable helpers - -// TODO -// TODO -// TODO -// x) Wrap metadata into its own struct -// x) Make metadata a struct vs a class +#nullable enable using System; using System.Runtime.CompilerServices; +using imm; - -static public class iu +/// +/// Helper static class for processing immutable objects using a 'ref' pattern. +/// Provides different levels of processing based on the type. +/// +public static class iu { - //This can handle both Timed and Recorded - static public T Process( ref T obj, Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExpression = default ) - where T : imm.Recorded, imm.Imm - { - obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression ); - return obj; - } + /// + /// Processes a 'Versioned' object (Level 1). + /// + public static T LightProcess( + ref T obj, + Func fn, + string reason = "Processed") + where T : Versioned + { + obj = obj.Process(fn, reason); + return obj; + } - static public imm.Timed Process( ref imm.Timed obj, Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExpression = default ) - where T : imm.Timed, imm.Imm - { - obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression ); - return obj; - } + /// + /// Processes a 'Recorded' object (Level 2), capturing caller info. + /// + public static T Process( + ref T obj, + Func fn, + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0, + [CallerArgumentExpression("fn")] string dbgExpression = "") + where T : Recorded // Catches Recorded and Timed + { + // This will call the 'Timed' override if T is Timed, + // or the 'Recorded' override if T is Recorded. + obj = obj.Process(fn, reason, dbgName, dbgPath, dbgLine, dbgExpression); + return obj; + } - - static public T LightProcess( ref T obj, Func fn, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0, - [CallerArgumentExpression("fn")] - string dbgExpression = default ) - where T : imm.Versioned, imm.Imm - { - obj = obj.Process( fn, reason ); - return obj; - } -} + // No specific Process needed for Timed, as it's caught by Recorded + // and its Process override handles the timing. +} \ No newline at end of file diff --git a/logging/Log.cs b/logging/Log.cs index fddd01f..06b590c 100644 --- a/logging/Log.cs +++ b/logging/Log.cs @@ -45,9 +45,9 @@ public record struct Value( T _val, string _exp = "" ) [CallerArgumentExpression("v")] string dbgExp = "" ) - { - return new( v, dbgExp ); - } + { + return new( v, dbgExp ); + } } public struct SourceLoc @@ -55,11 +55,11 @@ public struct SourceLoc readonly string _reason = ""; readonly string _dbgName = ""; readonly string _dbgPath = ""; - readonly int _dbgLine = -1; + readonly int _dbgLine = -1; public SourceLoc( string reason, string dbgName, string dbgPath, int dbgLine ) { - _reason = reason; + _reason = reason; _dbgName = dbgName; _dbgPath = dbgPath; _dbgLine = dbgLine; @@ -129,7 +129,7 @@ static public class log static void StartGCWatcher() { - while( !s_running ) + while( s_threading != ThreadState.Running ) { Thread.Sleep( 10 ); } @@ -140,7 +140,7 @@ static public class log static void StartTracing() { - while( !s_running ) + while( s_threading != ThreadState.Running ) { Thread.Sleep( 10 ); } @@ -154,9 +154,9 @@ static public class log [CallerArgumentExpression("val")] string dbgExp = "" ) - { - return new( val, dbgExp ); - } + { + return new( val, dbgExp ); + } [Flags] public enum LogType @@ -255,7 +255,7 @@ static public class log static public void endpointForCat( string cat, Endpoints ep ) { - ImmutableInterlocked.AddOrUpdate( ref s_logEPforCat, cat, ep, (k, v) => ep ); + ImmutableInterlocked.AddOrUpdate( ref s_logEPforCat, cat, ep, ( k, v ) => ep ); } @@ -284,7 +284,7 @@ static public class log static int s_cwdLength = s_cwd.Length; static ImmutableDictionary s_files = ImmutableDictionary.Empty; -#region Util + #region Util static public (string, string, int) record( [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1 ) => (dbgName, dbgPath, dbgLine); @@ -331,11 +331,11 @@ static public class log { return relativePath( path ); } -#endregion // Util + #endregion // Util -#region Forwards - static public T call( Func func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression("func")] string dbgExp = "" ) + #region Forwards + static public T call( Func func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" ) { log.info( $"Calling {dbgExp}" ); var val = func(); @@ -343,53 +343,53 @@ static public class log return val; } - static public T var( T val, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression("val")] string dbgExp = "" ) + static public T var( T val, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" ) { log.info( $"Called {dbgExp} Got: {val}" ); return val; } - static public void call( Action func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression("func")] string dbgExp = "" ) + static public void call( Action func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" ) { log.info( $"Calling {dbgExp}" ); func(); log.info( $"| Done" ); } - static public void fatal( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + static public void fatal( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.Fatal, path, line, member, cat, dbgExp, obj ); } [StackTraceHidden] - static public void error( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + static public void error( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.Error, path, line, member, cat, dbgExp, obj ); } [StackTraceHidden] - static public void warn( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + static public void warn( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.Warn, path, line, member, cat, dbgExp, obj ); } - static public void high( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + static public void high( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.High, path, line, member, cat, dbgExp, obj ); } static public void info( string msg, string cat = "", object? obj = null, - [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.Info, path, line, member, cat, dbgExp, obj ); } - static public void debug( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + static public void debug( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.Debug, path, line, member, cat, dbgExp, obj ); } - static public void trace( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("msg")] string dbgExp = "" ) + static public void trace( string msg, string cat = "", object? obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) { logBase( msg, LogType.Trace, path, line, member, cat, dbgExp, obj ); } @@ -401,19 +401,19 @@ static public class log { } - static public void info( string msg, A a, string cat = "", [CallerArgumentExpression("a")] string dbgExpA = "" ) + static public void info( string msg, A a, string cat = "", [CallerArgumentExpression( "a" )] string dbgExpA = "" ) { } - static public void info( string msg, object a, object b, string cat = "", [CallerArgumentExpression("a")] string dbgExpA = "", [CallerArgumentExpression("a")] string dbgExpB = "" ) + static public void info( string msg, object a, object b, string cat = "", [CallerArgumentExpression( "a" )] string dbgExpA = "", [CallerArgumentExpression( "a" )] string dbgExpB = "" ) { } #endregion -#region Helpers - static public void logProps( object obj, string header, LogType type = LogType.Debug, string cat = "", string prefix = "", [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression("obj")] string dbgExpObj = "" ) + #region Helpers + static public void logProps( object obj, string header, LogType type = LogType.Debug, string cat = "", string prefix = "", [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "" ) { var list = refl.GetAllProperties( obj.GetType() ); @@ -434,7 +434,7 @@ static public class log } catch( Exception ex ) { - logBase( $"Exception processing {pi.Name} {ex.Message}", LogType.Error, path, line, member, cat, dbgExpObj, obj); + logBase( $"Exception processing {pi.Name} {ex.Message}", LogType.Error, path, line, member, cat, dbgExpObj, obj ); } } @@ -454,7 +454,7 @@ static public class log log.warn( $"Got {notExpectedValue} instead of {value}{falseString}" ); } } -#endregion + #endregion static object s_lock = new object(); @@ -516,41 +516,41 @@ static public class log lock( s_lock ) { //We're already running, so just tell folks, and jump back - if( s_running ) + if( s_threading == ThreadState.Running ) { log.info( $"Already running, so this is a NOP" ); } - s_running = true; + s_threading = ThreadState.Running; - s_startTime = DateTime.Now; + s_startTime = DateTime.Now; - s_cwd = Directory.GetCurrentDirectory(); - s_cwdLength = s_cwd.Length; + s_cwd = Directory.GetCurrentDirectory(); + s_cwdLength = s_cwd.Length; - s_endpoints = endpoints; + s_endpoints = endpoints; - var dir = Path.GetDirectoryName( filename ); + var dir = Path.GetDirectoryName( filename ); - if( dir?.Length > 0 ) - { - Directory.CreateDirectory( dir ); - } + if( dir?.Length > 0 ) + { + Directory.CreateDirectory( dir ); + } - s_stream = new FileStream( filename, FileMode.Append, FileAccess.Write ); - s_writer = new StreamWriter( s_stream, Encoding.UTF8, 128, true ); + s_stream = new FileStream( filename, FileMode.Append, FileAccess.Write ); + s_writer = new StreamWriter( s_stream, Encoding.UTF8, 128, true ); - //s_errorStream = new FileStream( filename + ".error", FileMode.Append, FileAccess.Write ); - //s_errorWriter = new StreamWriter( s_errorStream ); + //s_errorStream = new FileStream( filename + ".error", FileMode.Append, FileAccess.Write ); + //s_errorWriter = new StreamWriter( s_errorStream ); - { + { var time = DateTime.Now; // Header for this run var blankLine = new LogEvent( LogType.Raw, $"", "", 0, "", "lib.time", "", null ); var beginLine = new LogEvent( LogType.Raw, $"Begin B E G I N ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); - var timeLine = new LogEvent( LogType.Raw, $"D A T E {time.Year}/{time.Month.ToString("00")}/{time.Day.ToString("00")} T I M E {time.Hour.ToString("00")}:{time.Minute.ToString("00")}:{time.Second.ToString("00")}.{time.Millisecond.ToString("000")}{time.Microsecond.ToString("000")}", "", 0, "", "lib.time", "", null ); + var timeLine = new LogEvent( LogType.Raw, $"D A T E {time.Year}/{time.Month.ToString( "00" )}/{time.Day.ToString( "00" )} T I M E {time.Hour.ToString( "00" )}:{time.Minute.ToString( "00" )}:{time.Second.ToString( "00" )}.{time.Millisecond.ToString( "000" )}{time.Microsecond.ToString( "000" )}", "", 0, "", "lib.time", "", null ); //writeToAll( endLine ); @@ -561,61 +561,88 @@ static public class log writeToAll( timeLine ); writeToAll( blankLine ); + } + + LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null ); + writeToAll( msgStartupBegin ); + + LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null ); + writeToAll( msgFilename ); + + StartThread(); + + + + //info( $"Logging in {filename}" ); + + + LogGC.RegisterObjectId( s_lock ); + + //Debug.Listeners.Add( this ); + + //var evt = CreateLogEvent( LogType.Info, $"startup", "System", null ); + + //s_events.Enqueue( evt ); + + /* + if( (endpoints & Endpoints.Console) == Endpoints.Console ) + { + addDelegate(WriteToConsole); + } + */ + + + //LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup END", "", 0, "", "lib.time", "", null ); + //writeToAll( msgStartupBegin ); + + info( $"startup END", cat: "log.startup" ); } - LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null ); - writeToAll( msgStartupBegin ); + } - LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null ); - writeToAll( msgFilename ); - - var start = new ThreadStart( run ); + private static void StartThread() + { + var start = new ThreadStart( threadLoop ); s_thread = new Thread( start ); s_thread.Priority = ThreadPriority.BelowNormal; s_thread.Name = $"Logging"; s_thread.Start(); - - - - //info( $"Logging in {filename}" ); - - - LogGC.RegisterObjectId( s_lock ); - - //Debug.Listeners.Add( this ); - - //var evt = CreateLogEvent( LogType.Info, $"startup", "System", null ); - - //s_events.Enqueue( evt ); - - /* - if( (endpoints & Endpoints.Console) == Endpoints.Console ) - { - addDelegate(WriteToConsole); - } - */ - - - //LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup END", "", 0, "", "lib.time", "", null ); - //writeToAll( msgStartupBegin ); - - info( $"startup END", cat: "log.startup" ); - } - } - static bool s_running = false; - - static void run() + private static void StopThread( ThreadState thread ) { - while( !s_running ) + log.info( $"Setting thread to {thread}"); + s_threading = thread; + int count = 0; + while( s_thread.IsAlive ) + { + Thread.Sleep( 0 ); + count++; + } + + Console.WriteLine( $"Waited {count} loops" ); + } + + public enum ThreadState + { + Invalid, + Running, + Paused, + Finished, + } + + static ThreadState s_threading = ThreadState.Invalid; + + static void threadLoop() + { + while( s_threading != ThreadState.Running ) { // TODO PERF Replace this with a semaphore/mutex Thread.Sleep( 1 ); } - while( s_running ) + while( s_threading != ThreadState.Running ) { while( s_events.TryDequeue( out var evt ) ) { @@ -626,22 +653,18 @@ static public class log Thread.Sleep( 1 ); } - var endLine = new LogEvent( LogType.Raw, $"End E N D ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); - writeToAll( endLine ); - + //var endLine = new LogEvent( LogType.Raw, $"End E N D ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); + var endLine = new LogEvent( LogType.Raw, $"Thread state {s_threading} ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); + writeToAll( endLine ); } public static void stop() { - if( !s_running ) return; + if( s_threading == ThreadState.Finished ) + return; - s_running = false; - - while( s_thread.IsAlive ) - { - Thread.Sleep( 0 ); - } + StopThread( ThreadState.Finished ); s_writer?.Close(); s_stream?.Close(); @@ -651,6 +674,24 @@ static public class log } + public static void pauseThread() + { + log.info("Pausing thread" ); + + s_threading = ThreadState.Paused; + + StopThread( ThreadState.Paused ); + } + + public static void unpauseThread() + { + s_threading = ThreadState.Running; + + StartThread(); + } + + + static public void addDelegate( Log_delegate cb ) { s_delegates.Add( cb ); @@ -660,14 +701,22 @@ static public class log { switch( type ) { - case LogType.Trace: return ' '; - case LogType.Debug: return ' '; - case LogType.Info: return ' '; - case LogType.High: return '+'; - case LogType.Warn: return '+'; - case LogType.Error: return '*'; - case LogType.Fatal: return '*'; - default: return '?'; + case LogType.Trace: + return ' '; + case LogType.Debug: + return ' '; + case LogType.Info: + return ' '; + case LogType.High: + return '+'; + case LogType.Warn: + return '+'; + case LogType.Error: + return '*'; + case LogType.Fatal: + return '*'; + default: + return '?'; } } @@ -725,7 +774,7 @@ static public class log var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) ); - var timeHdr = $"{s_timeHeader}{((int)span.TotalMinutes).ToString("000")}:{span.Seconds.ToString("D2")}.{span.Milliseconds.ToString("000")}"; + var timeHdr = $"{s_timeHeader}{( (int)span.TotalMinutes ).ToString( "000" )}:{span.Seconds.ToString( "D2" )}.{span.Milliseconds.ToString( "000" )}"; var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym ); @@ -750,17 +799,20 @@ static public class log var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) ); - if( string.IsNullOrWhiteSpace( truncatedCat) ) truncatedCat = $"B R O K E N truncatedCat"; + if( string.IsNullOrWhiteSpace( truncatedCat ) ) + truncatedCat = $"B R O K E N truncatedCat"; //Dont really need the year-month-day frankly. //var timeHdr = $"{evt.Time.Year}-{evt.Time.Month.ToString("00")}-{evt.Time.Day.ToString("00")} {evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")}{evt.Time.Microsecond.ToString("000")}"; - var timeHdr = $"{evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")}{evt.Time.Microsecond.ToString("000")}"; + var timeHdr = $"{evt.Time.Hour.ToString( "00" )}:{evt.Time.Minute.ToString( "00" )}:{evt.Time.Second.ToString( "00" )}.{evt.Time.Millisecond.ToString( "000" )}{evt.Time.Microsecond.ToString( "000" )}"; - if( string.IsNullOrWhiteSpace( timeHdr) ) timeHdr = $"B R O K E N timeHdr"; + if( string.IsNullOrWhiteSpace( timeHdr ) ) + timeHdr = $"B R O K E N timeHdr"; var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym ); - if( string.IsNullOrWhiteSpace( msgHdr) ) msgHdr = $"B R O K E N msgHdr"; + if( string.IsNullOrWhiteSpace( msgHdr ) ) + msgHdr = $"B R O K E N msgHdr"; return msgHdr; @@ -793,7 +845,7 @@ static public class log var msg = evt.Msg; - if( (string.IsNullOrWhiteSpace(msg) && evt.LogType != LogType.Raw) || msg.Contains( (char)0 ) ) + if( ( string.IsNullOrWhiteSpace( msg ) && evt.LogType != LogType.Raw ) || msg.Contains( (char)0 ) ) { msg = "B R O K E N msg"; } @@ -831,7 +883,7 @@ static public class log { s_lastDisplaySeconds = curSeconds; - var minuteEvt = new LogEvent( LogType.Raw, $"T I M E ==> {evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")} : {evt.Time.ToShortDateString()}", "", 0, "", "lib.time", "", null ); + var minuteEvt = new LogEvent( LogType.Raw, $"T I M E ==> {evt.Time.Hour.ToString( "00" )}:{evt.Time.Minute.ToString( "00" )}:{evt.Time.Second.ToString( "00" )}.{evt.Time.Millisecond.ToString( "000" )} : {evt.Time.ToShortDateString()}", "", 0, "", "lib.time", "", null ); minuteEvt.Time = evt.Time; writeSpecialEvent( minuteEvt ); } @@ -893,7 +945,7 @@ static public class log } catch( Exception ex ) { - #region Catch + #region Catch Console.WriteLine( "EXCEPTION DURING LOGGING" ); Console.WriteLine( "EXCEPTION DURING LOGGING" ); Console.WriteLine( "EXCEPTION DURING LOGGING" ); @@ -907,7 +959,7 @@ static public class log Debug.WriteLine( "EXCEPTION DURING LOGGING" ); Debug.WriteLine( "EXCEPTION DURING LOGGING" ); Debug.WriteLine( $"Exception {ex}" ); - #endregion + #endregion } } diff --git a/res/Resource.cs b/res/Resource.cs index 86ff5fe..a50323b 100644 --- a/res/Resource.cs +++ b/res/Resource.cs @@ -1,444 +1,434 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -//using System.Threading.Tasks; using System.Diagnostics; +using System.IO; using System.Reflection; using System.Collections.Immutable; -using System.Threading; -using System.IO; -using Microsoft.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.CodeAnalysis; // Note: This was in the original, but seems unused. Keep for now. +using System.Linq; #nullable enable namespace res; +// A delegate representing a function that can load a resource of type T. +public delegate T Load( string filename ); -using ImmDefLoad = ImmutableQueue<(string name, Ref)>; - -public interface Res_old +/// +/// Abstract base class for a resource reference. +/// Provides a common way to refer to resources and includes debugging/tracking info. +/// +[DebuggerDisplay( "Path = {Filename}" )] +public abstract class Ref { + public static bool VerboseLogging { get; set; } = false; + public string Filename { get; } + public string Reason { get; } + public string DbgName { get; } + public string DbgPath { get; } + public int DbgLine { get; } + + protected Ref( + string filename, + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ) + { + Filename = filename; + Reason = reason; + DbgName = dbgName; + DbgPath = dbgPath; + DbgLine = dbgLine; + + if( VerboseLogging ) + log.info( $"Ref Created: {GetType().Name} {Filename}" ); + } + + /// + /// Looks up and loads the resource. + /// + /// The loaded resource object. + public abstract object Lookup( + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ); + + /// + /// Called when the resource might have changed (optional, base implementation does nothing). + /// + public virtual void OnChange() { } + + /// + /// Internal method to trigger the initial load (used by deferred loading, if implemented). + /// + internal virtual void InternalLoad() { } } -[DebuggerDisplay("Path = {path}")] -abstract public class Ref : lib.I_Serialize -{ - static public bool s_verboseLogging = false; - - public string Filename =>path; - - - public Ref( string filename = "{empty_filename}", - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - { - path = filename; - if( s_verboseLogging ) log.info( $"Ref: {GetType().Name} {path}" ); - - _reason = reason; - _dbgName = dbgName; - _dbgPath = dbgPath; - _dbgLine = dbgLine; - } - - abstract public object lookup( - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - - ); - - - virtual public void OnChange() - { - } - - virtual internal void load() - { - - } - - private string path = "{set_from_inline_cons}"; - - protected string _reason = ""; - protected string _dbgName = ""; - protected string _dbgPath = ""; - protected int _dbgLine = 0; - protected string _dbgExp = ""; - - -} - +/// +/// A typed reference to a resource of type T. +/// Handles lazy loading and access to the resource. +/// [Serializable] -[DebuggerDisplay("Path = {path} / Res = {res}")] +[DebuggerDisplay( "Path = {Filename} / Res = {m_res}" )] public class Ref : Ref where T : class, new() { - public T res => m_res != null ? m_res : lookup(); + [NonSerialized] + private T? m_res; - override public T lookup( - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 + /// + /// Gets the resource, loading it if necessary. + /// + public T Res => m_res ?? Lookup(); - ) + /// + /// Gets the resource, loading it if necessary. + /// + //[Deprecated("Use Res property instead.")] + public T res => m_res ?? Lookup(); + + public Ref( + string filename = "", + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ) + : base( + !string.IsNullOrWhiteSpace( filename ) ? filename : $"{{{dbgName}_{Path.GetFileNameWithoutExtension( dbgPath )}}}", + reason, dbgName, dbgPath, dbgLine ) { - m_res = Mgr.load( Filename, $"Ref lookup", dbgName, dbgPath, dbgLine ); - if( s_verboseLogging ) log.info( $"Ref.lookup {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" ); - return m_res; - } - - //For serialization - /* - public Ref() - : - base( "{set_from_ref<>_default_cons}" ) - { - if( s_verboseLogging ) log.info( $"Ref {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" ); - } - */ - - - public Ref( string filename = "", - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - : - base( !string.IsNullOrWhiteSpace(filename) ? filename : $"{{{dbgName}_{log.whatFile(dbgPath)}}}" , reason, dbgName, dbgPath, dbgLine ) - { - if( s_verboseLogging ) log.info( $"Ref {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" ); + if( VerboseLogging ) + log.info( $"Ref Created: {GetType().Name}<{typeof( T ).Name}> {Filename}" ); m_res = default; } - - override internal void load() + /// + /// Looks up and loads the resource. + /// + public override T Lookup( + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ) { - m_res = Mgr.load( Filename, _reason, _dbgName, _dbgPath, _dbgLine ); - if( s_verboseLogging ) log.info( $"Ref.load {GetType().Name} {GetType().GenericTypeArguments[0]} path {Filename}" ); + // If already loaded, return it. + if( m_res != null ) + return m_res; + + // Load using the Mgr. + m_res = Mgr.Load( Filename, $"Ref lookup (orig: {Reason}) bcs {reason}", dbgName, dbgPath, dbgLine ); + if( VerboseLogging ) + log.info( $"Ref.Lookup: {GetType().Name}<{typeof( T ).Name}> {Filename}" ); + return m_res; } - public object OnDeserialize( object enclosing ) + /* + /// + /// Overrides the base Lookup to provide a typed result. + /// + public override object Lookup( + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0) { - return enclosing; + return Lookup(reason, dbgName, dbgPath, dbgLine); + } + */ + + /// + /// Internal load implementation. + /// + internal override void InternalLoad() + { + if( m_res == null ) + { + m_res = Mgr.Load( Filename, Reason, DbgName, DbgPath, DbgLine ); + if( VerboseLogging ) + log.info( $"Ref.InternalLoad: {GetType().Name}<{typeof( T ).Name}> {Filename}" ); + } } - static public Ref createAsset( T v, string path, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) + /// + /// Creates a new resource asset and saves it, returning a Ref to it. + /// Handles existing files by renaming them. + /// + public static Ref CreateAsset( + T value, string path, + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ) { if( File.Exists( path ) ) { - log.warn( $"For {typeof(T).Name}, saving asset to {path}, but it already exists" ); - - var newPath = $"{path}_{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToShortTimeString()}"; - - System.IO.File.Move(path, newPath ); - - log.warn( $"For {typeof(T).Name}, renamed to {newPath}" ); + log.warn( $"Asset exists: {path}. Renaming before creating." ); + var newPath = $"{path}_{DateTime.Now:yyyyMMdd_HHmmss}"; + File.Move( path, newPath ); + log.warn( $"Renamed existing asset to: {newPath}" ); } - var createReason = $"Type {v.GetType().Name}"; + // Here you would typically save 'value' to 'path' using a registered saver or specific logic. + // Since saving isn't defined, we'll assume it happens externally or add it here if needed. + // For now, we'll just log and create the Ref. + log.info( $"Creating asset Ref for {typeof( T ).Name} at {path}." ); - if( v is imm.Imm imm ) - { - createReason = $"{imm?.Meta}"; - } + // We need a way to save 'value' to 'path' before creating the Ref. + // This part requires a 'Save' function, which isn't in the original. + // Let's assume you'll add saving logic here. + // Mgr.Save(value, path); // Example: Needs implementation - var newRef = new Ref( path, $"createAsset {createReason} bcs {reason}", dbgName, dbgPath, dbgLine ); + var immMeta = (value as imm.Obj)?.Meta; + + + + var createReason = $"CreateAsset: {value.GetType().Name} - {immMeta?.Reason ?? "N/A"}"; + var newRef = new Ref( path, $"{createReason} bcs {reason}", dbgName, dbgPath, dbgLine ); + + // We should make the newRef hold the 'value' immediately, + // or ensure loading it back gives the same 'value'. + // Setting m_res directly might be an option, or caching it. + newRef.m_res = value; + Mgr.CacheResource( path, value, $"CreateAsset {typeof( T ).Name}" ); return newRef; } - - [NonSerialized] - protected T? m_res; } -public class Resource +/// +/// Holds a weak reference to a cached resource along with metadata. +/// +internal record ResourceHolder( WeakReference WeakRef, string Name, DateTime Captured ) where T : class; + +/// +/// Manages resource loading, caching, and loader registration. +/// This static class replaces the original `Mgr` instance. +/// +public static class Mgr { - static public Mgr mgr = new(); -} - -public delegate T Load( string filename ); - - -abstract class LoadHolder -{ - public abstract object load(); -} - - -class LoadHolder : LoadHolder -{ - public LoadHolder( Load fnLoad ) + // Internal holder for type-specific loaders. + private abstract class LoadHolder { public abstract object Load( string filename ); } + private class LoadHolder : LoadHolder { - _fnLoad = fnLoad; + private readonly Load _fnLoad; + public LoadHolder( Load fnLoad ) { _fnLoad = fnLoad; } + public override object Load( string filename ) => _fnLoad( filename )!; } - public Load _fnLoad; - - public override object load() - { - return load(); - } -} - -public record class ResourceHolder( WeakReference weak, string Name, DateTime captured ) : imm.Recorded> -where T : class -{ - -} - -//generic classes make a new static per generic type -class ResCache where T : class, new() -{ - public static T s_default = new(); - public static ImmutableDictionary> s_cache = ImmutableDictionary>.Empty; -} - - -public class Mgr -{ - - static public void startup() + // Cache for resource holders (contains WeakReferences). + private static class ResCache where T : class, new() { + public static T s_default = new(); + public static ImmutableDictionary> s_cache = ImmutableDictionary>.Empty; } - static public void register( Load loader ) + private static ImmutableDictionary s_loaders = ImmutableDictionary.Empty; + // Using ConcurrentDictionary for per-file locking objects. + private static readonly System.Collections.Concurrent.ConcurrentDictionary s_loadingLocks = new(); + + /// + /// Registers a specific loading function for a type T. + /// + public static void Register( Load loader ) where T : class, new() { - Debug.Assert( !Resource.mgr.m_loaders.ContainsKey( typeof( T ) ) ); - - var lh = new LoadHolder( loader ); - - ImmutableInterlocked.TryAdd( ref Resource.mgr.m_loaders, typeof( T ), lh ); - } - - //Register all subclasses of a particular type - //???? Should we just always do this? - static public void registerSub() - { - registerSub( typeof(T) ); - } - - static public void registerSub( Type baseType ) - { - log.info( $"Registering loader for {baseType.Name}" ); - - Type[] typeParams = new Type[1]; - foreach( var mi in baseType.GetMethods() ) + var type = typeof( T ); + if( s_loaders.ContainsKey( type ) ) { - if( mi.Name == "res_load" && mi.IsGenericMethod ) + log.warn( $"Loader for type {type.Name} is already registered. Overwriting." ); + } + ImmutableInterlocked.Update( ref s_loaders, d => d.SetItem( type, new LoadHolder( loader ) ) ); + log.info( $"Registered loader for {type.Name}" ); + } + + /// + /// Registers loaders for all subtypes of T found in loaded assemblies, + /// assuming they follow the 'res_load' static generic method pattern. + /// + public static void RegisterSub() => RegisterSub( typeof( T ) ); + + /// + /// Registers loaders for all subtypes of a base type. + /// + public static void RegisterSub( Type baseType ) + { + log.info( $"Scanning for subtypes of {baseType.Name} to register loaders..." ); + MethodInfo? genericLoadMethod = baseType.GetMethods( BindingFlags.Public | BindingFlags.Static ) + .FirstOrDefault( mi => mi.Name == "res_load" && mi.IsGenericMethodDefinition ); + + if( genericLoadMethod == null ) + { + log.error( $"Could not find 'public static T res_load(string filename)' method on {baseType.Name} or its bases." ); + return; + } + + foreach( var ass in AppDomain.CurrentDomain.GetAssemblies() ) + { + try { - foreach( var ass in AppDomain.CurrentDomain.GetAssemblies() ) + foreach( var t in ass.GetTypes() ) { - foreach( var t in ass.GetTypes() ) + if( t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom( t ) && t != baseType ) { - if( !baseType.IsAssignableFrom( t ) ) - continue; + try + { + log.debug( $"Found subtype {t.Name}, creating loader." ); + var concreteLoadMethod = genericLoadMethod.MakeGenericMethod( t ); + var loadDelegateType = typeof( Load<> ).MakeGenericType( t ); + var loaderDelegate = Delegate.CreateDelegate( loadDelegateType, concreteLoadMethod ); - log.debug( $"Making a loader for {t.Name}" ); + var loadHolderType = typeof( LoadHolder<> ).MakeGenericType( t ); + var loadHolder = Activator.CreateInstance( loadHolderType, loaderDelegate ) as LoadHolder; - typeParams[0] = t; - var mi_ng = mi.MakeGenericMethod( typeParams ); - - var loadGenType = typeof( Load<> ); - - var loadType = loadGenType.MakeGenericType( t ); - - var loader = Delegate.CreateDelegate( loadType, mi_ng ); - - var lhGenType = typeof( LoadHolder<> ); - - var lhType = lhGenType.MakeGenericType( t ); - - var lh = Activator.CreateInstance( lhType, loader ) as LoadHolder; - - ImmutableInterlocked.TryAdd( ref Resource.mgr.m_loaders!, t, lh ); + if( loadHolder != null ) + { + if( !ImmutableInterlocked.TryAdd( ref s_loaders, t, loadHolder ) ) + { + log.debug( $"Loader for {t.Name} already exists, skipping." ); + } + else + { + log.info( $"Registered loader for subtype {t.Name}." ); + } + } + } + catch( Exception ex ) + { + log.error( $"Failed to create loader for subtype {t.Name}: {ex.Message}" ); + } } } - return; + } + catch( ReflectionTypeLoadException rtle ) + { + log.warn( $"Could not fully scan assembly {ass.FullName}: {rtle.Message}" ); + } + catch( Exception ex ) + { + log.warn( $"Error scanning assembly {ass.FullName}: {ex.Message}" ); } } } - static public Ref lookup( string filename, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) where T : class, new() + /// + /// Creates a Ref for a given filename. + /// + public static Ref Lookup( + string filename, + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ) where T : class, new() { return new Ref( filename, reason, dbgName, dbgPath, dbgLine ); } - /* - - static public Ref lookup( string filename, Type t, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) + /// + /// Loads a resource, handling caching and thread-safe loading. + /// + public static T Load( + string filename, + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 ) where T : class, new() { - return new Ref( filename, reason, dbgName, dbgPath, dbgLine ); + // 1. Check cache first (no lock) + if( TryGetFromCache( filename, out var cachedValue ) ) + { + return cachedValue; + } + + // 2. Get a lock specific to this filename and lock it. + var fileLock = s_loadingLocks.GetOrAdd( filename, _ => new object() ); + lock( fileLock ) + { + // 3. Double-check cache *inside* the lock + if( TryGetFromCache( filename, out cachedValue ) ) + { + return cachedValue; + } + + // 4. Perform the actual load + log.warn( $"Loading {typeof( T ).Name}: {filename} ({reason} at {dbgName}:{dbgLine})" ); + var newValue = ActualLoad( filename, reason, dbgName, dbgPath, dbgLine ); + + // 5. Cache the new value + CacheResource( filename, newValue, reason ); + + return newValue; + } } - // @@@ TODO Pass information through here - static public T? load( string filename ) where T : class + /// + /// Manually adds or updates a resource in the cache. + /// + internal static void CacheResource( string filename, T resource, string reason ) where T : class, new() + { + var weak = new WeakReference( resource ); + var holder = new ResourceHolder( weak, filename, DateTime.Now ); + ImmutableInterlocked.Update( ref ResCache.s_cache, d => d.SetItem( filename, holder ) ); + log.info( $"Cached {typeof( T ).Name}: {filename} ({reason})" ); + } + + + /// + /// Tries to retrieve a resource from the cache. + /// + private static bool TryGetFromCache( string filename, out T value ) where T : class, new() { if( ResCache.s_cache.TryGetValue( filename, out var holder ) ) { - - if( holder.weak.TryGetTarget( out var v ) ) + if( holder.WeakRef.TryGetTarget( out var v ) ) { - return v; + value = v; + return true; } - - log.info( $"{filename} was in cache, but its been dropped, reloading." ); + log.info( $"{filename} was in cache but dropped (GC'd), needs reloading." ); } - - log.warn( $"Block Loading {filename}." ); - - var newV = actualLoad( filename ); - - return newV; - } - */ - - static public T load( string filename, - string reason = "", - [CallerMemberName] string dbgName = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) where T : class, new() - { - if( ResCache.s_cache.TryGetValue( filename, out var holder ) ) - { - if( holder.weak.TryGetTarget( out var v ) ) - { - return v; - } - - log.info( $"{filename} was in cache, but its been dropped, reloading." ); - } - - log.warn( $"Block Loading {filename}." ); - - var newV = actualLoad( filename ); - - if( newV is imm.Imm imm ) - { - newV = (T)imm.Record( $"Loading because {reason}", dbgName, dbgPath, dbgLine ); - } - - return newV; + value = default!; + return false; } - static public T actualLoad( string filename ) where T : class, new() + /// + /// The core loading logic. Must be called within a lock. + /// + private static T ActualLoad( + string filename, + string reason = "", + [CallerMemberName] string dbgName = "", + [CallerFilePath] string dbgPath = "", + [CallerLineNumber] int dbgLine = 0 + ) where T : class, new() { - lock(s_loadingLock) + if( s_loaders.TryGetValue( typeof( T ), out var loaderHolder ) ) { - if( s_loading.TryGetValue( filename, out var evt ) ) + var loadedObject = loaderHolder.Load( filename ); + if( loadedObject is T value ) { - if( ResCache.s_cache.TryGetValue( filename, out var holder ) ) + var meta = (value as imm.Obj)?.Meta; + + // If it's an immutable object, record its loading. + if( value is imm.Obj imm ) { - if( holder.weak.TryGetTarget( out var v ) ) - { - log.trace( $"{typeof(T).Name} loading {filename}" ); - return v; - } - - log.error( $"{filename} was in cache, but its been dropped, reloading." ); + return (T)imm.Record( $"Loading bcs {reason}", dbgName, dbgPath, dbgLine ); } + return value; } - + else { - if( Resource.mgr.m_loaders.TryGetValue( typeof( T ), out var loaderGen ) ) - { - var loader = loaderGen as LoadHolder; - - var v = loader!._fnLoad( filename ); - - var weak = new WeakReference( v ); - - var holder = new ResourceHolder( weak, $"", DateTime.Now ).Record(); - - log.info( $"To {typeof(T).Name} add {filename}" ); - - var alreadyAdded = !ImmutableInterlocked.TryAdd( ref ResCache.s_cache, filename, holder ); - - if( alreadyAdded ) - { - log.error( $"Key {filename} already existed, though it shouldnt." ); - } - - return v; - } - else - { - log.error( $"Loader could not be found for type {typeof( T )}" ); - - return ResCache.s_default; - } + log.error( $"Loader for {typeof( T ).Name} returned wrong type: {loadedObject?.GetType().Name}" ); + return ResCache.s_default; } } - - //return actualLoad( filename ); - } - - static object s_loadingLock = new object(); - - static ImmutableDictionary s_loading = ImmutableDictionary.Empty; - //static ImmDefLoad s_deferredLoad = ImmDefLoad.Empty; - - - public Mgr() - { - log.info( $"Creating Res.Mgr" ); - - /* - var ts = new ThreadStart( deferredLoader ); - - m_deferredLoader = new Thread( ts ); - */ - - LogGC.RegisterObjectId( s_loadingLock ); - - //m_deferredLoader.Start(); - } - - /* - void deferredLoader() - { - while( true ) + else { - Thread.Sleep( 1 ); - - if( ImmutableInterlocked.TryDequeue( ref s_deferredLoad, out var v ) ) - { - v.Item2.load(); - } + log.error( $"Loader could not be found for type {typeof( T ).Name} for file {filename}" ); + return ResCache.s_default; } } - //*/ - - - - ImmutableDictionary m_loaders = ImmutableDictionary.Empty; - - //Thread m_deferredLoader; - } - diff --git a/ser/SerializableDictionary.cs b/ser/SerializableDictionary.cs_bad similarity index 100% rename from ser/SerializableDictionary.cs rename to ser/SerializableDictionary.cs_bad diff --git a/ser/VersionFormatter.cs b/ser/VersionFormatter.cs_bad similarity index 100% rename from ser/VersionFormatter.cs rename to ser/VersionFormatter.cs_bad diff --git a/ser/XmlFormatter.cs b/ser/XmlFormatter.cs_bad similarity index 99% rename from ser/XmlFormatter.cs rename to ser/XmlFormatter.cs_bad index 7f0fa03..f485f17 100644 --- a/ser/XmlFormatter.cs +++ b/ser/XmlFormatter.cs_bad @@ -17,7 +17,7 @@ using System.Reflection; namespace lib { //Old, use 2 now. - class XmlFormatter: IFormatter + class XmlFormatter_BAD: IFormatter { StreamingContext m_context; //SerializationMode m_mode; diff --git a/ser/XmlFormatter2.cs b/ser/XmlFormatter2.cs index 93b194a..0315d63 100644 --- a/ser/XmlFormatter2.cs +++ b/ser/XmlFormatter2.cs @@ -45,6 +45,7 @@ namespace lib object OnDeserialize( object enclosing ) => this; } + [Flags] public enum Types { @@ -543,7 +544,7 @@ namespace lib { if( _cfg.VerboseLogging ) log.info( $"" ); - var isImm = typeof(imm.Imm).IsAssignableFrom( narrowType ); + var isImm = typeof(imm.Obj).IsAssignableFrom( narrowType ); XmlNodeList allChildren = elem.ChildNodes; @@ -701,7 +702,7 @@ namespace lib } else { - var imm = obj as imm.Imm; + var imm = obj as imm.Obj; var newObj = imm.Record( $"From XML {fromStr}:{elem.ParentNode?.Name}{elem.Name}" ); return newObj; } @@ -1343,7 +1344,7 @@ namespace lib HashSet whitelistFields, whitelistProps; GetFilters( _cfg.TypesDefault, mi, narrowType, out filterFields, out filterProps, out doImpls, out doFields, out doProps, out whitelistFields, out whitelistProps ); - var isImm = typeof(imm.Imm).IsAssignableFrom( narrowType ); + var isImm = typeof(imm.Obj).IsAssignableFrom( narrowType ); if( doFields || doImpls ) { diff --git a/ser/XmlSer.cs b/ser/XmlSer.cs new file mode 100644 index 0000000..0c71ad6 --- /dev/null +++ b/ser/XmlSer.cs @@ -0,0 +1,917 @@ +using System; +using System.IO; +using System.Xml; +using System.Runtime.Serialization; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System.Collections.Immutable; +using System.Net.Sockets; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Linq.Expressions; + +namespace serdes; + +#region Attributes & Enums (Mostly unchanged, ensure these exist) + +// Ensure I_Serialize, Types, lib.Ser, lib.Do, lib.Dont, lib.ChildAttribute, etc., +// exist as you defined them in XmlFormatter2.cs. + +// --- Enums & Records (Slightly adjusted/renamed) --- +public enum Datastructure { Tree, Graph } +public enum BackingFieldNaming { Short, Regular } +public enum POD { Attributes, Elements } +public record struct TypeProxy( Func ser, Func des ); + +public record XmlCfg : imm.Recorded +{ + public bool Verbose { get; init; } = false; + public Datastructure Structure { get; init; } = Datastructure.Tree; + public int Version { get; init; } = 2; + public ImmutableDictionary> WLProps { get; init; } = ImmutableDictionary>.Empty; + public ImmutableDictionary> WLFields { get; init; } = ImmutableDictionary>.Empty; + public ImmutableDictionary Proxies { get; init; } = ImmutableDictionary.Empty; + public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Short; + public POD POD { get; init; } = POD.Attributes; + public lib.Types TypesDefault { get; init; } = lib.Types.Fields; + public static XmlCfg Default { get; } = new XmlCfg(); +} + +#endregion + +#region Reflection & Metadata Cache + +public record MemberMeta( + MemberInfo Info, + Type Type, + string XmlName, + Func GetValue, + Action SetValue, + bool IsPodAttribute, + bool HasDo, + bool HasDont +); + +public record TypeSerializationInfo( + Type Type, + List Members, + bool IsISerializable, + bool IsImm, + bool IsProxy, + TypeProxy? ProxyDef +); + +public class TypeMetaCache +{ + private readonly ConcurrentDictionary _cache = new(); + private readonly XmlCfg _cfg; + + public TypeMetaCache( XmlCfg cfg ) => _cfg = cfg; + + public TypeSerializationInfo Get( Type type ) => _cache.GetOrAdd( type, BuildTypeInfo ); + + // Helper to create accessors (using standard reflection - can be optimized) + private static Func CreateGetter( MemberInfo mi ) + { + if( mi is FieldInfo fi ) + return fi.GetValue; + if( mi is PropertyInfo pi && pi.CanRead ) + return pi.GetValue; + return _ => null; + } + + private static Action CreateSetter( MemberInfo mi ) + { + if( mi is FieldInfo fi ) + return fi.SetValue; + if( mi is PropertyInfo pi && pi.CanWrite ) + return pi.SetValue; + return ( _, _ ) => { }; + } + + private TypeSerializationInfo BuildTypeInfo( Type type ) + { + var members = new List(); + bool filterFields, filterProps, doImpls, doFields, doProps; + HashSet whitelistFields, whitelistProps; + + // Use null for MemberInfo as GetFilters works on Type level here + GetFilters( _cfg.TypesDefault, null, type, out filterFields, out filterProps, out doImpls, out doFields, out doProps, out whitelistFields, out whitelistProps ); + + var isImm = typeof( imm.Obj ).IsAssignableFrom( type ); + + if( doFields || doImpls ) + { + foreach( var fi in refl.GetAllFields( type ) ) + { + ProcessMember( fi, filterFields, doImpls, whitelistFields, isImm, members ); + } + } + + if( doProps || doImpls ) + { + foreach( var pi in refl.GetAllProperties( type ) ) + { + ProcessMember( pi, filterProps, doImpls, whitelistProps, isImm, members ); + } + } + + var (isProxy, proxyDef) = FindProxy( type ); + + return new TypeSerializationInfo( + type, + members, + typeof( ISerializable ).IsAssignableFrom( type ) && !typeof( Delegate ).IsAssignableFrom( type ), // Exclude Delegates + isImm, + isProxy, + proxyDef + ); + } + + private (bool, TypeProxy?) FindProxy( Type type ) + { + var tryType = type; + while( tryType != null && tryType != typeof( object ) ) + { + if( _cfg.Proxies.TryGetValue( tryType, out var proxy ) ) + { + return (true, proxy); + } + tryType = tryType.BaseType; + } + return (false, null); + } + + private void ProcessMember( MemberInfo mi, bool filter, bool doImpls, HashSet whitelist, bool isImm, List members ) + { + var (hasDo, hasDont, propName) = GetMemberAttributes( mi, out var actualMiForAtts ); + + if( hasDont ) + return; + if( isImm && ( mi.Name == "MetaStorage" || mi.Name == "Fn" ) ) + return; + if( mi.GetCustomAttribute( true ) != null ) + return; + + string name = mi.Name; + string finalName = name; + + if( !string.IsNullOrEmpty( propName ) ) + { + finalName = ( _cfg.Naming == BackingFieldNaming.Short ) ? propName : name; + } + + finalName = refl.TypeToIdentifier( finalName ); // Ensure XML-safe name + + if( !hasDo && FilterField( filter, doImpls, whitelist, actualMiForAtts, mi.Name ) ) // Filter based on original name + return; + + var type = ( mi is FieldInfo fi ) ? fi.FieldType : ( (PropertyInfo)mi ).PropertyType; + bool isPod = Type.GetTypeCode( type ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( type ); // Simplified POD check + + members.Add( new MemberMeta( + mi, + type, + finalName, + CreateGetter( mi ), + CreateSetter( mi ), + isPod && _cfg.POD == POD.Attributes, + hasDo, + hasDont + ) ); + } + + private (bool hasDo, bool hasDont, string propName) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi ) + { + actualMi = mi; + string propName = ""; + bool isBacking = mi.Name.StartsWith( "<" ) && mi.Name.EndsWith( "BackingField" ); + + if( isBacking && mi is FieldInfo ) + { + var gtIndex = mi.Name.IndexOf( '>' ); + propName = mi.Name.Substring( 1, gtIndex - 1 ); + var propInfo = mi.DeclaringType?.GetProperty( propName ); + if( propInfo != null ) + actualMi = propInfo; + } + + return ( + actualMi.GetCustomAttribute() != null, + actualMi.GetCustomAttribute() != null, + propName + ); + } + + + // --- These helpers are copied/adapted from XmlFormatter2 --- + private static bool FilterField( bool filter, bool doImpls, HashSet whitelist, MemberInfo mi, string name ) + { + if( doImpls && mi.GetCustomAttribute( true ) == null ) + return true; + if( filter && !whitelist.Contains( refl.TypeToIdentifier( name ) ) ) + return true; // Check against XML-safe name + return false; + } + + private static void GetFilters( lib.Types typesDefault, MemberInfo? mi, Type type, out bool filterFields, out bool filterProps, out bool doImpls, out bool doFields, out bool doProps, out HashSet whitelistFields, out HashSet whitelistProps ) + { + var custWLFields = mi?.GetCustomAttribute( true ); + var custWLProps = mi?.GetCustomAttribute( true ); + + filterFields = custWLFields != null; + filterProps = custWLProps != null; + + var typesTodo = type.GetCustomAttribute( true )?.Types ?? typesDefault; + + doImpls = typesTodo.HasFlag(lib.Types.Implied ); + doFields = filterFields || typesTodo.HasFlag(lib.Types.Fields ); + doProps = filterProps || typesTodo.HasFlag(lib.Types.Props ); + whitelistFields = new( custWLFields?.Values?.Select( refl.TypeToIdentifier ) ?? Enumerable.Empty() ); + whitelistProps = new( custWLProps?.Values?.Select( refl.TypeToIdentifier ) ?? Enumerable.Empty() ); + } +} + +public class TypeResolver +{ + private readonly ConcurrentDictionary _cache = new(); + private readonly Assembly[] _assemblies; + private static readonly FormatterConverter _conv = new(); + + public TypeResolver() + { + _assemblies = AppDomain.CurrentDomain.GetAssemblies(); + } + + public Type Resolve( XmlElement elem, Type? expectedType ) + { + if( elem.HasAttribute( "_.t" ) ) + { + var typeName = elem.GetAttribute( "_.t" ); + var resolved = FindType( typeName ); + if( resolved != null ) + return resolved; + } + return expectedType ?? typeof( object ); // Fallback needed + } + + public Type? FindType( string typeName ) + { + return _cache.GetOrAdd( typeName, tn => + { + // Try direct lookup first (might work for fully qualified) + var t = Type.GetType( tn ); + if( t != null ) + return t; + + // Then search assemblies + foreach( Assembly a in _assemblies ) + { + t = a.GetType( tn ); + if( t != null ) + return t; + } + log.warn( $"Could not resolve type: {tn}" ); + return null; + } ); + } + + + public object ConvertSimple( string value, Type type ) + { + if( type.IsEnum ) + return Enum.Parse( type, value ); + try + { + return _conv.Convert( value, type ); + } + catch( Exception ex ) + { + log.warn( $"Conversion failed for '{value}' to {type.Name}: {ex.Message}. Returning default." ); + return type.IsValueType ? Activator.CreateInstance( type ) : null; + } + } +} + +#endregion + +#region Type Handlers + +public interface ITypeHandler +{ + bool CanHandle( TypeSerializationInfo typeInfo, XmlElement? elem = null ); // Elem needed for Deser + void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ); + object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ); +} + +// --- Primitive Handler --- +public class PrimitiveHandler : ITypeHandler +{ + public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => Type.GetTypeCode( ti.Type ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( ti.Type ); + + public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + { + string val = elem.HasAttribute( "v" ) ? elem.GetAttribute( "v" ) : elem.InnerText; + if( val == "null" ) + return null; + return ser._resolver.ConvertSimple( val, expectedType ); + } + + public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { + writer.WriteStartElement( name ); + writer.WriteAttributeString( "v", "null" ); + writer.WriteEndElement(); + return; + } + + bool writeElements = ser._cfg.POD == POD.Elements || forceType || !( writer is XmlTextWriter ); + if( !writeElements && writer is XmlTextWriter tw ) + writeElements = tw.WriteState != WriteState.Element; + + if( writeElements ) + writer.WriteStartElement( name ); + + if( forceType || ser._cfg.POD == POD.Elements ) + { + if( forceType ) + writer.WriteAttributeString( "_.t", obj.GetType().FullName ); + writer.WriteAttributeString( "v", obj.ToString() ); + } + else + { + writer.WriteAttributeString( name, obj.ToString() ); + } + + if( writeElements ) + writer.WriteEndElement(); + } +} + +// --- Proxy Handler --- +public class ProxyHandler : ITypeHandler +{ + public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => ti.IsProxy || ( elem?.HasAttribute( "proxy" ) ?? false ); + + + public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + { + var ti = ser._meta.Get( expectedType ); // Re-get to ensure we have proxy info + if( !elem.HasAttribute( "proxy" ) || !ti.ProxyDef.HasValue ) + { + log.warn( $"Proxy read failed for {expectedType.Name}. Fallback needed." ); + return null; // Should fall back or throw + } + var proxyVal = elem.GetAttribute( "proxy" ); + return ti.ProxyDef.Value.des( expectedType.FullName, proxyVal ); + } + + public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { ser.GetHandler( typeof( object ) ).WriteXml( ser, writer, null, name, memberType, forceType ); return; } + + var ti = ser._meta.Get( obj.GetType() ); + if( !ti.ProxyDef.HasValue ) + { log.error( "Proxy write called without proxy def!" ); return; } + + writer.WriteStartElement( name ); + var proxyStr = ti.ProxyDef.Value.ser( obj ); + writer.WriteAttributeString( "proxy", proxyStr ); + writer.WriteEndElement(); + } +} + +// --- ISerializable Handler --- +public class ISerializableHandler : ITypeHandler +{ + public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => ti.IsISerializable; + + public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + { + // Create/Get instance (needs FormatterServices for ISerializable) + object obj = existing ?? FormatterServices.GetUninitializedObject( expectedType ); + long id = ser.TrackIfGraph( obj, elem ); // Track it + + var serInfo = new SerializationInfo( expectedType, new FormatterConverter() ); + + foreach( XmlNode objNode in elem.ChildNodes ) + { + if( objNode is XmlElement childElem ) + { + string childName = childElem.Name; + Type? childType = ser._resolver.FindType( childElem.GetAttribute( "_.t" ) ); + if( childType != null ) + { + var desValue = ser.ReadNode( childElem, childType, null ); + serInfo.AddValue( childName, desValue, childType ); + } + } + } + + var context = new StreamingContext( StreamingContextStates.All ); // Or use ser.Context + var cons = expectedType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, new[] { typeof( SerializationInfo ), typeof( StreamingContext ) }, null ); + + if( cons != null ) + { + cons.Invoke( obj, new object[] { serInfo, context } ); + } + else + { + log.error( $"ISerializable type {expectedType.Name} lacks the required constructor." ); + } + + if( obj is IDeserializationCallback cb ) + cb.OnDeserialization( obj ); + + return obj; + } + + public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { /* Write null */ return; } + if( !( obj is ISerializable serObj ) ) + { /* Error */ return; } + + writer.WriteStartElement( name ); + ser.WriteTypeAttr( writer, memberType, obj.GetType() ); + + if( ser.HandleGraphWrite( writer, obj, out bool first ) ) + { + if( first ) + { + var serInfo = new SerializationInfo( obj.GetType(), new FormatterConverter() ); + var context = new StreamingContext( StreamingContextStates.All ); + serObj.GetObjectData( serInfo, context ); + + foreach( var member in serInfo ) + { + ser.WriteNode( writer, member.Value, refl.TypeToIdentifier( member.Name ), member.ObjectType, true ); // Force type for ISer + } + } + } + writer.WriteEndElement(); + } +} + +// --- Collection Handler --- +public class CollectionHandler : ITypeHandler +{ + public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => + typeof( IEnumerable ).IsAssignableFrom( ti.Type ) && ti.Type != typeof( string ); + + public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + { + // Determine element type + Type elemType = GetElementType( expectedType ); + + // Create a temporary list + var listType = typeof( List<> ).MakeGenericType( elemType ); + var list = (IList)Activator.CreateInstance( listType )!; + + ser.TrackIfGraph( list, elem ); // Track list if graph + + // Populate the list + foreach( XmlNode node in elem.ChildNodes ) + { + if( node is XmlElement childElem ) + { + list.Add( ser.ReadNode( childElem, elemType, null ) ); + } + } + + // Convert to the final expected type (Array, Immutable*, List) + return ConvertToFinalCollection( list, expectedType, elemType ); + } + + public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { /* Write null */ return; } + if( !( obj is IEnumerable collection ) ) + { /* Error */ return; } + + writer.WriteStartElement( name ); + ser.WriteTypeAttr( writer, memberType, obj.GetType() ); + + if( ser.HandleGraphWrite( writer, obj, out bool first ) ) + { + if( first ) + { + Type elemType = GetElementType( obj.GetType() ); + int i = 0; + foreach( var item in collection ) + { + ser.WriteNode( writer, item, $"i{i++}", elemType, false ); + } + } + } + writer.WriteEndElement(); + } + + private Type GetElementType( Type collectionType ) + { + if( collectionType.IsArray ) + return collectionType.GetElementType()!; + if( collectionType.IsGenericType ) + { + var args = collectionType.GetGenericArguments(); + if( args.Length == 1 ) + return args[0]; + if( args.Length == 2 ) + return typeof( KeyValuePair<,> ).MakeGenericType( args ); + } + return typeof( object ); // Fallback + } + + private object ConvertToFinalCollection( IList list, Type expectedType, Type elemType ) + { + if( expectedType.IsArray ) + { + var arr = Array.CreateInstance( elemType, list.Count ); + list.CopyTo( arr, 0 ); + return arr; + } + if( expectedType.IsGenericType ) + { + var genDef = expectedType.GetGenericTypeDefinition(); + if( genDef == typeof( ImmutableArray<> ) ) + { + var method = typeof( ImmutableArray ).GetMethods() + .First( m => m.Name == "ToImmutableArray" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) + .MakeGenericMethod( elemType ); + return method.Invoke( null, new object[] { list } )!; + } + if( genDef == typeof( ImmutableList<> ) ) + { + var method = typeof( ImmutableList ).GetMethods() + .First( m => m.Name == "ToImmutableList" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) + .MakeGenericMethod( elemType ); + return method.Invoke( null, new object[] { list } )!; + } + // Add more immutable/dictionary handlers here (using MakeImmutableDictionary etc.) + } + return list; // Default to List if no specific match + } +} + + +// --- Object Handler (Default/Complex) --- +public class ObjectHandler : ITypeHandler +{ + public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => true; // Fallback + + public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + { + var actualType = ser._resolver.Resolve( elem, expectedType ); + var ti = ser._meta.Get( actualType ); + + // 1. Get/Create Instance + var (obj, _) = GetOrCreateInstance( ser, elem, actualType, existing ); + if( obj == null ) + return null; + + // Handle graph refs (if already processed) + if( ser._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) + { + long id = long.Parse( elem.GetAttribute( "ref" ) ); + if( ser._processed.TryGetValue( id, out var processedObj ) ) + return processedObj; + } + + // Track if it's new + ser.TrackIfGraph( obj, elem ); + + // 2. Hydrate + foreach( var memberMeta in ti.Members ) + { + var (valueSource, isAttribute) = FindValueSource( elem, memberMeta.XmlName ); + + if( valueSource != null ) + { + object? memberValue; + object? currentMemberValue = memberMeta.GetValue( obj ); + + if( isAttribute ) + { + memberValue = ser._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type ); + } + else // Child Element + { + memberValue = ser.ReadNode( (XmlElement)valueSource, memberMeta.Type, currentMemberValue ); + } + + // Set value, respecting lib.Do/lib.Dont and pre-hydration + if( ShouldSetValue( memberMeta, existing != null ) ) + { + memberMeta.SetValue( obj, memberValue ); + } + } + } + + // 3. Post-processing + if( obj is lib.I_Serialize iSer ) + obj = iSer.OnDeserialize( null ); + if( ti.IsImm && obj is imm.Obj immObj ) + return immObj.Record( $"From XML {elem.Name}" ); + + return obj; + } + + public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { /* Write null */ return; } + + writer.WriteStartElement( name ); + ser.WriteTypeAttr( writer, memberType, obj.GetType() ); + var ti = ser._meta.Get( obj.GetType() ); + + if( ser.HandleGraphWrite( writer, obj, out bool first ) ) + { + if( first ) + { + foreach( var memberMeta in ti.Members ) + { + var value = memberMeta.GetValue( obj ); + if( value != null ) + { + // If POD-Attribute, write attribute + if( memberMeta.IsPodAttribute ) + { + writer.WriteAttributeString( memberMeta.XmlName, value.ToString() ); + } + else // Else, write element + { + ser.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false ); + } + } + } + } + } + writer.WriteEndElement(); + } + + private (XmlNode? source, bool isAttribute) FindValueSource( XmlElement parent, string name ) + { + if( parent.HasAttribute( name ) ) + { + return (parent.Attributes[name], true); + } + foreach( XmlNode node in parent.ChildNodes ) + { + if( node.NodeType == XmlNodeType.Element && node.Name == name ) + { + return (node, false); + } + } + return (null, false); + } + + private bool ShouldSetValue( MemberMeta member, bool isHydrating ) + { + // If we have a 'lib.Do' attribute, always set it. + if( member.HasDo ) + return true; + // If we are *not* hydrating (i.e., creating new), always set. + if( !isHydrating ) + return true; + // If we *are* hydrating, only set if it *doesn't* have 'lib.Do' (since we already checked) + // This implies a 'merge' - only setting values that were explicitly marked. + // You might need to refine this based on your exact 'merge' semantics. + // A common approach is to *only* set if 'lib.Do' is present when hydrating. + // Let's assume: Set if New, or if Hydrating AND HasDo. + return !isHydrating || member.HasDo; // Revisit this logic based on desired merge. + // Original `XmlFormatter2` seemed to set unless `lib.Dont` was present, + // and it didn't seem to have strong pre-hydration checks *during* SetValue. + // This needs clarification. For now, let's set unless `lib.Dont`. + // return !member.HasDont; // <-- Simpler, maybe closer? + } + + + private (object? obj, long id) GetOrCreateInstance( XmlSer ser, XmlElement elem, Type type, object? existing ) + { + long id = -1; + bool first = true; + + // Check existing + if( existing != null && type.IsAssignableFrom( existing.GetType() ) ) + { + id = ser._idGen.GetId( existing, out first ); + return (existing, id); + } + + // Create new + object? newObj = null; + try + { + if( type.GetConstructor( Type.EmptyTypes ) != null ) + { + newObj = Activator.CreateInstance( type ); + } + else + { + newObj = FormatterServices.GetUninitializedObject( type ); + } + } + catch( Exception ex ) + { + log.error( $"Failed to create instance of {type.Name}: {ex.Message}" ); + return (null, -1); + } + + id = ser._idGen.GetId( newObj, out first ); + return (newObj, id); + } +} + +#endregion + +#region XmlSer (Coordinator) + +public class XmlSer // : IFormatter +{ + internal readonly XmlCfg _cfg; + internal readonly TypeMetaCache _meta; + internal readonly TypeResolver _resolver; + private readonly List _handlers; + + // Per-operation state + internal ObjectIDGenerator _idGen = new(); + internal Dictionary _processed = new(); + private string _streamSource = ""; + + public XmlSer( XmlCfg? cfg = null ) + { + _cfg = cfg ?? XmlCfg.Default; + _meta = new TypeMetaCache( _cfg ); + _resolver = new TypeResolver(); + _handlers = new List + { + new ProxyHandler(), + new ISerializableHandler(), + new PrimitiveHandler(), + new CollectionHandler(), + new ObjectHandler() // Must be last + }; + if( _cfg.Verbose ) + log.high( "XmlSer Initialized." ); + } + + internal ITypeHandler GetHandler( Type t, XmlElement? elem = null ) + { + var ti = _meta.Get( t ); + return _handlers.First( h => h.CanHandle( ti, elem ) ); + } + + // --- Context Helpers --- + internal void WriteTypeAttr( XmlWriter writer, Type memberType, Type actualType ) + { + if( memberType != actualType ) + { + writer.WriteAttributeString( "_.t", actualType.FullName ); + } + } + + internal bool HandleGraphWrite( XmlWriter writer, object obj, out bool first ) + { + first = true; + if( _cfg.Structure == Datastructure.Graph ) + { + long id = _idGen.GetId( obj, out first ); + writer.WriteAttributeString( "ref", id.ToString() ); + if( first ) + _processed[id] = obj; + } + return first || _cfg.Structure == Datastructure.Tree; // Write if first or if Tree + } + + internal long TrackIfGraph( object obj, XmlElement elem ) + { + long id = -1; + bool first; + if( _cfg.Structure == Datastructure.Graph ) + { + id = _idGen.GetId( obj, out first ); + if( elem.HasAttribute( "ref" ) ) + { + id = long.Parse( elem.GetAttribute( "ref" ) ); + } + if( !_processed.ContainsKey( id ) ) + { + _processed[id] = obj; + } + } + return id; + } + + + // --- Deserialization --- + public T? Deserialize( Stream stream ) => (T?)Deserialize( stream, typeof( T ) ); + + public object? Deserialize( Stream stream, Type? type = null ) + { + _streamSource = stream.ToString() ?? "{null}"; // Basic source, improve as needed + _processed.Clear(); + _idGen = new ObjectIDGenerator(); + + using var reader = XmlReader.Create( stream, new XmlReaderSettings { IgnoreWhitespace = true } ); + XmlDocument doc = new XmlDocument(); + try + { doc.Load( reader ); } + catch( Exception ex ) { log.error( $"XML Load failed: {ex.Message}" ); return null; } + + if( doc.DocumentElement == null ) + return null; + + return ReadNode( doc.DocumentElement, type ?? typeof( object ), null ); + } + + public void DeserializeInto( Stream stream, T obj ) where T : class + { + _streamSource = stream.ToString() ?? "{null}"; + _processed.Clear(); + _idGen = new ObjectIDGenerator(); + + using var reader = XmlReader.Create( stream, new XmlReaderSettings { IgnoreWhitespace = true } ); + XmlDocument doc = new XmlDocument(); + try + { doc.Load( reader ); } + catch( Exception ex ) { log.error( $"XML Load failed: {ex.Message}" ); return; } + + if( doc.DocumentElement == null ) + return; + + ReadNode( doc.DocumentElement, typeof( T ), obj ); + } + + + internal object? ReadNode( XmlElement elem, Type expectedType, object? existing ) + { + if( elem.HasAttribute( "v" ) && elem.GetAttribute( "v" ) == "null" ) + return null; + + // 1. Handle refs (if Graph) + if( _cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) + { + long id = long.Parse( elem.GetAttribute( "ref" ) ); + if( _processed.TryGetValue( id, out var obj ) ) + return obj; + } + + // 2. Determine Type & Select Handler + var actualType = _resolver.Resolve( elem, expectedType ); + var ti = _meta.Get( actualType ); + var handler = _handlers.First( h => h.CanHandle( ti, elem ) ); + + // 3. Delegate + return handler.ReadXml( this, elem, actualType, existing ); + } + + // --- Serialization --- + public void Serialize( Stream stream, object root ) + { + _processed.Clear(); + _idGen = new ObjectIDGenerator(); + + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = System.Text.Encoding.UTF8, // Use UTF8 for better compatibility + OmitXmlDeclaration = true // Often preferred for fragments/storage + }; + + using var writer = XmlWriter.Create( stream, settings ); + + writer.WriteStartDocument(); + WriteNode( writer, root, "root", root?.GetType() ?? typeof( object ), true ); // Force type on root + writer.WriteEndDocument(); + writer.Flush(); + } + + internal void WriteNode( XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + { + if( obj == null ) + { + writer.WriteStartElement( name ); + writer.WriteAttributeString( "v", "null" ); + writer.WriteEndElement(); + return; + } + + var actualType = obj.GetType(); + var ti = _meta.Get( actualType ); + var handler = _handlers.First( h => h.CanHandle( ti ) ); + + handler.WriteXml( this, writer, obj, name, memberType, forceType || memberType != actualType ); + } +} +#endregion