feat: add initial project setup
This commit is contained in:
commit
ed1e31314d
32 changed files with 3397 additions and 0 deletions
397
.editorconfig
Normal file
397
.editorconfig
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[*.{md,json,yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{csproj,props,targets,slnx,config}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.{cs,vb}]
|
||||||
|
#### code style rule default severity ####
|
||||||
|
dotnet_analyzer_diagnostic.category-style.severity = warning
|
||||||
|
|
||||||
|
#### .NET Coding Conventions ####
|
||||||
|
[*.{cs,vb}]
|
||||||
|
# Organize usings
|
||||||
|
file_header_template = Copyright (c) The Geekeey Authors\nSPDX-License-Identifier: EUPL-1.2
|
||||||
|
dotnet_separate_import_directive_groups = true
|
||||||
|
dotnet_sort_system_directives_first = true
|
||||||
|
|
||||||
|
# this. and Me. preferences
|
||||||
|
dotnet_style_qualification_for_event = false
|
||||||
|
dotnet_style_qualification_for_field = false
|
||||||
|
dotnet_style_qualification_for_method = false
|
||||||
|
dotnet_style_qualification_for_property = false
|
||||||
|
|
||||||
|
# Language keywords vs BCL types preferences
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||||
|
dotnet_style_predefined_type_for_member_access = true
|
||||||
|
|
||||||
|
# Parentheses preferences
|
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||||
|
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
dotnet_diagnostic.IDE0270.severity = none
|
||||||
|
dotnet_style_coalesce_expression = true
|
||||||
|
|
||||||
|
dotnet_style_collection_initializer = true
|
||||||
|
dotnet_style_explicit_tuple_names = true
|
||||||
|
dotnet_style_null_propagation = true
|
||||||
|
dotnet_style_object_initializer = true
|
||||||
|
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||||
|
dotnet_style_prefer_auto_properties = true
|
||||||
|
dotnet_style_prefer_compound_assignment = true
|
||||||
|
|
||||||
|
dotnet_diagnostic.IDE0045.severity = suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||||
|
|
||||||
|
dotnet_diagnostic.IDE0046.severity = suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true
|
||||||
|
|
||||||
|
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||||
|
dotnet_style_prefer_inferred_tuple_names = true
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||||
|
dotnet_style_prefer_simplified_interpolation = true
|
||||||
|
dotnet_style_namespace_match_folder = false
|
||||||
|
|
||||||
|
# Field preferences
|
||||||
|
dotnet_style_readonly_field = true
|
||||||
|
|
||||||
|
# Suppression preferences
|
||||||
|
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||||
|
|
||||||
|
#### C# Coding Conventions ####
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
# var preferences
|
||||||
|
csharp_style_var_for_built_in_types = true
|
||||||
|
csharp_style_var_when_type_is_apparent = true
|
||||||
|
csharp_style_var_elsewhere = true
|
||||||
|
|
||||||
|
# Expression-bodied members
|
||||||
|
csharp_style_expression_bodied_accessors = true
|
||||||
|
csharp_style_expression_bodied_constructors = false
|
||||||
|
csharp_style_expression_bodied_indexers = true
|
||||||
|
csharp_style_expression_bodied_lambdas = true
|
||||||
|
csharp_style_expression_bodied_local_functions = false
|
||||||
|
csharp_style_expression_bodied_methods = false
|
||||||
|
csharp_style_expression_bodied_operators = false
|
||||||
|
csharp_style_expression_bodied_properties = true
|
||||||
|
|
||||||
|
# Pattern matching preferences
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true
|
||||||
|
csharp_style_prefer_not_pattern = true
|
||||||
|
csharp_style_prefer_pattern_matching = true
|
||||||
|
csharp_style_prefer_switch_expression = true
|
||||||
|
|
||||||
|
# Null-checking preferences
|
||||||
|
csharp_style_conditional_delegate_call = true
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
csharp_prefer_static_local_function = true
|
||||||
|
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
|
||||||
|
|
||||||
|
# Code-block preferences
|
||||||
|
csharp_prefer_braces = true
|
||||||
|
csharp_prefer_simple_using_statement = true
|
||||||
|
csharp_style_prefer_top_level_statements = false
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
csharp_prefer_simple_default_expression = true
|
||||||
|
csharp_style_deconstructed_variable_declaration = true
|
||||||
|
csharp_style_inlined_variable_declaration = true
|
||||||
|
csharp_style_pattern_local_over_anonymous_function = true
|
||||||
|
csharp_style_prefer_index_operator = true
|
||||||
|
csharp_style_prefer_range_operator = true
|
||||||
|
csharp_style_throw_expression = true
|
||||||
|
|
||||||
|
dotnet_diagnostic.IDE0058.severity = suggestion
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||||
|
|
||||||
|
# 'using' directive preferences
|
||||||
|
csharp_using_directive_placement = outside_namespace
|
||||||
|
|
||||||
|
# 'namespace' preferences
|
||||||
|
csharp_style_namespace_declarations = file_scoped
|
||||||
|
|
||||||
|
# 'constructor' preferences
|
||||||
|
csharp_style_prefer_primary_constructors = false
|
||||||
|
|
||||||
|
#### C# Formatting Rules ####
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
|
# Indentation preferences
|
||||||
|
csharp_indent_block_contents = true
|
||||||
|
csharp_indent_braces = false
|
||||||
|
csharp_indent_case_contents = true
|
||||||
|
csharp_indent_case_contents_when_block = true
|
||||||
|
csharp_indent_labels = one_less_than_current
|
||||||
|
csharp_indent_switch_labels = true
|
||||||
|
|
||||||
|
# Space preferences
|
||||||
|
csharp_space_after_cast = false
|
||||||
|
csharp_space_after_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_after_comma = true
|
||||||
|
csharp_space_after_dot = false
|
||||||
|
csharp_space_after_keywords_in_control_flow_statements = true
|
||||||
|
csharp_space_after_semicolon_in_for_statement = true
|
||||||
|
csharp_space_around_binary_operators = before_and_after
|
||||||
|
csharp_space_around_declaration_statements = false
|
||||||
|
csharp_space_before_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_before_comma = false
|
||||||
|
csharp_space_before_dot = false
|
||||||
|
csharp_space_before_open_square_brackets = false
|
||||||
|
csharp_space_before_semicolon_in_for_statement = false
|
||||||
|
csharp_space_between_empty_square_brackets = false
|
||||||
|
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||||
|
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||||
|
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_parentheses = false
|
||||||
|
csharp_space_between_square_brackets = false
|
||||||
|
|
||||||
|
# Wrapping preferences
|
||||||
|
csharp_preserve_single_line_blocks = true
|
||||||
|
csharp_preserve_single_line_statements = true
|
||||||
|
|
||||||
|
#### .NET Naming styles ####
|
||||||
|
[*.{cs,vb}]
|
||||||
|
|
||||||
|
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
|
||||||
|
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
|
||||||
|
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
|
||||||
|
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interfaces.applicable_kinds = interface
|
||||||
|
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.interfaces.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
|
||||||
|
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.enums.applicable_kinds = enum
|
||||||
|
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.enums.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
|
||||||
|
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
|
||||||
|
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
|
||||||
|
dotnet_naming_symbols.type_parameters.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
|
||||||
|
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.methods.applicable_kinds = method
|
||||||
|
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.methods.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
|
||||||
|
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.parameters.applicable_kinds = parameter
|
||||||
|
dotnet_naming_symbols.parameters.applicable_accessibilities = *
|
||||||
|
dotnet_naming_symbols.parameters.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
|
||||||
|
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.properties.applicable_kinds = property
|
||||||
|
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.properties.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
|
||||||
|
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.events.applicable_kinds = event
|
||||||
|
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.events.required_modifiers =
|
||||||
|
|
||||||
|
# local
|
||||||
|
|
||||||
|
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
|
||||||
|
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.local_variables.applicable_kinds = local
|
||||||
|
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
|
||||||
|
dotnet_naming_symbols.local_variables.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
|
||||||
|
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
|
||||||
|
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
|
||||||
|
dotnet_naming_symbols.local_functions.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
|
||||||
|
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.local_constants.applicable_kinds = local
|
||||||
|
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
|
||||||
|
dotnet_naming_symbols.local_constants.required_modifiers = const
|
||||||
|
|
||||||
|
# private
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
|
||||||
|
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.private_fields.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
|
||||||
|
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.private_static_fields.required_modifiers = static
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
|
||||||
|
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
|
||||||
|
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
|
||||||
|
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.public_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
|
||||||
|
dotnet_naming_symbols.public_fields.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
|
||||||
|
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
|
||||||
|
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
|
||||||
|
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
|
||||||
|
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
|
||||||
|
|
||||||
|
# others
|
||||||
|
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
|
||||||
|
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||||
|
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.pascalcase.required_prefix =
|
||||||
|
dotnet_naming_style.pascalcase.required_suffix =
|
||||||
|
dotnet_naming_style.pascalcase.word_separator =
|
||||||
|
dotnet_naming_style.pascalcase.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.ipascalcase.required_prefix = I
|
||||||
|
dotnet_naming_style.ipascalcase.required_suffix =
|
||||||
|
dotnet_naming_style.ipascalcase.word_separator =
|
||||||
|
dotnet_naming_style.ipascalcase.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.tpascalcase.required_prefix = T
|
||||||
|
dotnet_naming_style.tpascalcase.required_suffix =
|
||||||
|
dotnet_naming_style.tpascalcase.word_separator =
|
||||||
|
dotnet_naming_style.tpascalcase.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style._camelcase.required_prefix = _
|
||||||
|
dotnet_naming_style._camelcase.required_suffix =
|
||||||
|
dotnet_naming_style._camelcase.word_separator =
|
||||||
|
dotnet_naming_style._camelcase.capitalization = camel_case
|
||||||
|
|
||||||
|
dotnet_naming_style.camelcase.required_prefix =
|
||||||
|
dotnet_naming_style.camelcase.required_suffix =
|
||||||
|
dotnet_naming_style.camelcase.word_separator =
|
||||||
|
dotnet_naming_style.camelcase.capitalization = camel_case
|
||||||
|
|
||||||
|
dotnet_naming_style.s_camelcase.required_prefix = s_
|
||||||
|
dotnet_naming_style.s_camelcase.required_suffix =
|
||||||
|
dotnet_naming_style.s_camelcase.word_separator =
|
||||||
|
dotnet_naming_style.s_camelcase.capitalization = camel_case
|
||||||
43
.forgejo/workflows/default.yml
Normal file
43
.forgejo/workflows/default.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
name: default
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "develop" ]
|
||||||
|
paths-ignore:
|
||||||
|
- "doc/**"
|
||||||
|
- "*.md"
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main", "develop" ]
|
||||||
|
paths-ignore:
|
||||||
|
- "doc/**"
|
||||||
|
- "*.md"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
default:
|
||||||
|
name: dotnet-default-workflow
|
||||||
|
runs-on: debian-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dotnet-version: [ "10.0" ]
|
||||||
|
container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }}
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: https://code.geekeey.de/actions/checkout@1
|
||||||
|
|
||||||
|
- name: nuget login
|
||||||
|
run: |
|
||||||
|
# This token is readonly and can only be used for restore
|
||||||
|
dotnet nuget update source geekeey --store-password-in-clear-text \
|
||||||
|
--username "${{ github.actor }}" --password "${{ github.token }}"
|
||||||
|
|
||||||
|
- name: dotnet pack
|
||||||
|
run: |
|
||||||
|
dotnet pack -p:ContinuousIntegrationBuild=true
|
||||||
|
|
||||||
|
- name: dotnet format --verify-no-changes
|
||||||
|
run: |
|
||||||
|
dotnet format --no-restore --verify-no-changes --verbosity normal
|
||||||
|
|
||||||
|
- name: dotnet test
|
||||||
|
run: |
|
||||||
|
dotnet test -p:ContinuousIntegrationBuild=true
|
||||||
40
.forgejo/workflows/release.yml
Normal file
40
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [ "[0-9]+.[0-9]+.[0-9]+" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: dotnet-release-workflow
|
||||||
|
runs-on: debian-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dotnet-version: [ "10.0" ]
|
||||||
|
container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }}
|
||||||
|
steps:
|
||||||
|
- uses: https://code.geekeey.de/actions/checkout@1
|
||||||
|
|
||||||
|
- name: nuget login
|
||||||
|
run: |
|
||||||
|
# This token is readonly and can only be used for restore
|
||||||
|
dotnet nuget update source geekeey --store-password-in-clear-text \
|
||||||
|
--username "${{ github.actor }}" --password "${{ github.token }}"
|
||||||
|
|
||||||
|
- name: dotnet pack
|
||||||
|
run: |
|
||||||
|
dotnet pack -p:ContinuousIntegrationBuild=true
|
||||||
|
|
||||||
|
- name: dotnet format --verify-no-changes
|
||||||
|
run: |
|
||||||
|
dotnet format --no-restore --verify-no-changes --verbosity normal
|
||||||
|
|
||||||
|
- name: dotnet test
|
||||||
|
run: |
|
||||||
|
dotnet test -p:ContinuousIntegrationBuild=true
|
||||||
|
|
||||||
|
- name: dotnet nuget push
|
||||||
|
run: |
|
||||||
|
# The token used here is only intended to publish packages
|
||||||
|
dotnet nuget push -k "${{ secrets.geekeey_package_registry }}" \
|
||||||
|
artifacts/package/release/Geekeey.*.nupkg
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
artifacts/
|
||||||
|
*.DotSettings.user
|
||||||
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-05-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- This is the initial release of the library.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
[1.0.0]: https://code.geekeey.de/geekeey/semver/releases/tag/1.0.0
|
||||||
|
[Unreleased]: https://code.geekeey.de/geekeey/semver/compare/1.0.0...HEAD
|
||||||
37
Directory.Build.props
Normal file
37
Directory.Build.props
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup Condition="'$(ArtifactsPath)' == ''">
|
||||||
|
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<VersionPrefix>1.0.0</VersionPrefix>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
|
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||||
|
<WarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</WarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Label="NuGet Package Info">
|
||||||
|
<Authors>The Geekeey Team</Authors>
|
||||||
|
<Copyright>Copyright (c) The Geekeey Team 2026</Copyright>
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.SourceLink.Gitea" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<NuGetAuditLevel>moderate</NuGetAuditLevel>
|
||||||
|
<NuGetAuditMode>all</NuGetAuditMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
2
Directory.Build.targets
Normal file
2
Directory.Build.targets
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<Project>
|
||||||
|
</Project>
|
||||||
9
Directory.Packages.props
Normal file
9
Directory.Packages.props
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Microsoft.SourceLink.Gitea" Version="10.0.102" />
|
||||||
|
<PackageVersion Include="TUnit" Version="1.11.51" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
287
LICENSE.md
Normal file
287
LICENSE.md
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||||
|
EUPL © the European Union 2007, 2016
|
||||||
|
|
||||||
|
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||||
|
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||||
|
other than as authorised under this Licence is prohibited (to the extent such
|
||||||
|
use is covered by a right of the copyright holder of the Work).
|
||||||
|
|
||||||
|
The Work is provided under the terms of this Licence when the Licensor (as
|
||||||
|
defined below) has placed the following notice immediately following the
|
||||||
|
copyright notice for the Work:
|
||||||
|
|
||||||
|
Licensed under the EUPL
|
||||||
|
|
||||||
|
or has expressed by any other means his willingness to license under the EUPL.
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
In this Licence, the following terms have the following meaning:
|
||||||
|
|
||||||
|
- ‘The Licence’: this Licence.
|
||||||
|
|
||||||
|
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||||
|
Licensor under this Licence, available as Source Code and also as Executable
|
||||||
|
Code as the case may be.
|
||||||
|
|
||||||
|
- ‘Derivative Works’: the works or software that could be created by the
|
||||||
|
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||||
|
does not define the extent of modification or dependence on the Original Work
|
||||||
|
required in order to classify a work as a Derivative Work; this extent is
|
||||||
|
determined by copyright law applicable in the country mentioned in Article 15.
|
||||||
|
|
||||||
|
- ‘The Work’: the Original Work or its Derivative Works.
|
||||||
|
|
||||||
|
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||||
|
convenient for people to study and modify.
|
||||||
|
|
||||||
|
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||||
|
meant to be interpreted by a computer as a program.
|
||||||
|
|
||||||
|
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||||
|
the Work under the Licence.
|
||||||
|
|
||||||
|
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||||
|
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||||
|
|
||||||
|
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||||
|
the Work under the terms of the Licence.
|
||||||
|
|
||||||
|
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||||
|
renting, distributing, communicating, transmitting, or otherwise making
|
||||||
|
available, online or offline, copies of the Work or providing access to its
|
||||||
|
essential functionalities at the disposal of any other natural or legal
|
||||||
|
person.
|
||||||
|
|
||||||
|
2. Scope of the rights granted by the Licence
|
||||||
|
|
||||||
|
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||||
|
sublicensable licence to do the following, for the duration of copyright vested
|
||||||
|
in the Original Work:
|
||||||
|
|
||||||
|
- use the Work in any circumstance and for all usage,
|
||||||
|
- reproduce the Work,
|
||||||
|
- modify the Work, and make Derivative Works based upon the Work,
|
||||||
|
- communicate to the public, including the right to make available or display
|
||||||
|
the Work or copies thereof to the public and perform publicly, as the case may
|
||||||
|
be, the Work,
|
||||||
|
- distribute the Work or copies thereof,
|
||||||
|
- lend and rent the Work or copies thereof,
|
||||||
|
- sublicense rights in the Work or copies thereof.
|
||||||
|
|
||||||
|
Those rights can be exercised on any media, supports and formats, whether now
|
||||||
|
known or later invented, as far as the applicable law permits so.
|
||||||
|
|
||||||
|
In the countries where moral rights apply, the Licensor waives his right to
|
||||||
|
exercise his moral right to the extent allowed by law in order to make effective
|
||||||
|
the licence of the economic rights here above listed.
|
||||||
|
|
||||||
|
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||||
|
any patents held by the Licensor, to the extent necessary to make use of the
|
||||||
|
rights granted on the Work under this Licence.
|
||||||
|
|
||||||
|
3. Communication of the Source Code
|
||||||
|
|
||||||
|
The Licensor may provide the Work either in its Source Code form, or as
|
||||||
|
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||||
|
provides in addition a machine-readable copy of the Source Code of the Work
|
||||||
|
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||||
|
a notice following the copyright notice attached to the Work, a repository where
|
||||||
|
the Source Code is easily and freely accessible for as long as the Licensor
|
||||||
|
continues to distribute or communicate the Work.
|
||||||
|
|
||||||
|
4. Limitations on copyright
|
||||||
|
|
||||||
|
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||||
|
any exception or limitation to the exclusive rights of the rights owners in the
|
||||||
|
Work, of the exhaustion of those rights or of other applicable limitations
|
||||||
|
thereto.
|
||||||
|
|
||||||
|
5. Obligations of the Licensee
|
||||||
|
|
||||||
|
The grant of the rights mentioned above is subject to some restrictions and
|
||||||
|
obligations imposed on the Licensee. Those obligations are the following:
|
||||||
|
|
||||||
|
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||||
|
trademarks notices and all notices that refer to the Licence and to the
|
||||||
|
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||||
|
copy of the Licence with every copy of the Work he/she distributes or
|
||||||
|
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||||
|
notices stating that the Work has been modified and the date of modification.
|
||||||
|
|
||||||
|
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||||
|
Original Works or Derivative Works, this Distribution or Communication will be
|
||||||
|
done under the terms of this Licence or of a later version of this Licence
|
||||||
|
unless the Original Work is expressly distributed only under this version of the
|
||||||
|
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||||
|
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||||
|
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||||
|
|
||||||
|
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||||
|
Works or copies thereof based upon both the Work and another work licensed under
|
||||||
|
a Compatible Licence, this Distribution or Communication can be done under the
|
||||||
|
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||||
|
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||||
|
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||||
|
his/her obligations under this Licence, the obligations of the Compatible
|
||||||
|
Licence shall prevail.
|
||||||
|
|
||||||
|
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||||
|
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||||
|
a repository where this Source will be easily and freely available for as long
|
||||||
|
as the Licensee continues to distribute or communicate the Work.
|
||||||
|
|
||||||
|
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||||
|
trademarks, service marks, or names of the Licensor, except as required for
|
||||||
|
reasonable and customary use in describing the origin of the Work and
|
||||||
|
reproducing the content of the copyright notice.
|
||||||
|
|
||||||
|
6. Chain of Authorship
|
||||||
|
|
||||||
|
The original Licensor warrants that the copyright in the Original Work granted
|
||||||
|
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||||
|
power and authority to grant the Licence.
|
||||||
|
|
||||||
|
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||||
|
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||||
|
power and authority to grant the Licence.
|
||||||
|
|
||||||
|
Each time You accept the Licence, the original Licensor and subsequent
|
||||||
|
Contributors grant You a licence to their contributions to the Work, under the
|
||||||
|
terms of this Licence.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty
|
||||||
|
|
||||||
|
The Work is a work in progress, which is continuously improved by numerous
|
||||||
|
Contributors. It is not a finished work and may therefore contain defects or
|
||||||
|
‘bugs’ inherent to this type of development.
|
||||||
|
|
||||||
|
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||||
|
and without warranties of any kind concerning the Work, including without
|
||||||
|
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||||
|
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||||
|
copyright as stated in Article 6 of this Licence.
|
||||||
|
|
||||||
|
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||||
|
for the grant of any rights to the Work.
|
||||||
|
|
||||||
|
8. Disclaimer of Liability
|
||||||
|
|
||||||
|
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||||
|
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||||
|
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||||
|
of the Work, including without limitation, damages for loss of goodwill, work
|
||||||
|
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||||
|
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||||
|
However, the Licensor will be liable under statutory product liability laws as
|
||||||
|
far such laws apply to the Work.
|
||||||
|
|
||||||
|
9. Additional agreements
|
||||||
|
|
||||||
|
While distributing the Work, You may choose to conclude an additional agreement,
|
||||||
|
defining obligations or services consistent with this Licence. However, if
|
||||||
|
accepting obligations, You may act only on your own behalf and on your sole
|
||||||
|
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||||
|
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||||
|
for any liability incurred by, or claims asserted against such Contributor by
|
||||||
|
the fact You have accepted any warranty or additional liability.
|
||||||
|
|
||||||
|
10. Acceptance of the Licence
|
||||||
|
|
||||||
|
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||||
|
placed under the bottom of a window displaying the text of this Licence or by
|
||||||
|
affirming consent in any other similar way, in accordance with the rules of
|
||||||
|
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||||
|
acceptance of this Licence and all of its terms and conditions.
|
||||||
|
|
||||||
|
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||||
|
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||||
|
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||||
|
Distribution or Communication by You of the Work or copies thereof.
|
||||||
|
|
||||||
|
11. Information to the public
|
||||||
|
|
||||||
|
In case of any Distribution or Communication of the Work by means of electronic
|
||||||
|
communication by You (for example, by offering to download the Work from a
|
||||||
|
remote location) the distribution channel or media (for example, a website) must
|
||||||
|
at least provide to the public the information requested by the applicable law
|
||||||
|
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||||
|
stored and reproduced by the Licensee.
|
||||||
|
|
||||||
|
12. Termination of the Licence
|
||||||
|
|
||||||
|
The Licence and the rights granted hereunder will terminate automatically upon
|
||||||
|
any breach by the Licensee of the terms of the Licence.
|
||||||
|
|
||||||
|
Such a termination will not terminate the licences of any person who has
|
||||||
|
received the Work from the Licensee under the Licence, provided such persons
|
||||||
|
remain in full compliance with the Licence.
|
||||||
|
|
||||||
|
13. Miscellaneous
|
||||||
|
|
||||||
|
Without prejudice of Article 9 above, the Licence represents the complete
|
||||||
|
agreement between the Parties as to the Work.
|
||||||
|
|
||||||
|
If any provision of the Licence is invalid or unenforceable under applicable
|
||||||
|
law, this will not affect the validity or enforceability of the Licence as a
|
||||||
|
whole. Such provision will be construed or reformed so as necessary to make it
|
||||||
|
valid and enforceable.
|
||||||
|
|
||||||
|
The European Commission may publish other linguistic versions or new versions of
|
||||||
|
this Licence or updated versions of the Appendix, so far this is required and
|
||||||
|
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||||
|
versions of the Licence will be published with a unique version number.
|
||||||
|
|
||||||
|
All linguistic versions of this Licence, approved by the European Commission,
|
||||||
|
have identical value. Parties can take advantage of the linguistic version of
|
||||||
|
their choice.
|
||||||
|
|
||||||
|
14. Jurisdiction
|
||||||
|
|
||||||
|
Without prejudice to specific agreement between parties,
|
||||||
|
|
||||||
|
- any litigation resulting from the interpretation of this License, arising
|
||||||
|
between the European Union institutions, bodies, offices or agencies, as a
|
||||||
|
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||||
|
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||||
|
the Functioning of the European Union,
|
||||||
|
|
||||||
|
- any litigation arising between other parties and resulting from the
|
||||||
|
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||||
|
of the competent court where the Licensor resides or conducts its primary
|
||||||
|
business.
|
||||||
|
|
||||||
|
15. Applicable Law
|
||||||
|
|
||||||
|
Without prejudice to specific agreement between parties,
|
||||||
|
|
||||||
|
- this Licence shall be governed by the law of the European Union Member State
|
||||||
|
where the Licensor has his seat, resides or has his registered office,
|
||||||
|
|
||||||
|
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||||
|
residence or registered office inside a European Union Member State.
|
||||||
|
|
||||||
|
Appendix
|
||||||
|
|
||||||
|
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||||
|
|
||||||
|
- GNU General Public License (GPL) v. 2, v. 3
|
||||||
|
- GNU Affero General Public License (AGPL) v. 3
|
||||||
|
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||||
|
- Eclipse Public License (EPL) v. 1.0
|
||||||
|
- CeCILL v. 2.0, v. 2.1
|
||||||
|
- Mozilla Public Licence (MPL) v. 2
|
||||||
|
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||||
|
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||||
|
works other than software
|
||||||
|
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||||
|
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||||
|
Reciprocity (LiLiQ-R+).
|
||||||
|
|
||||||
|
The European Commission may update this Appendix to later versions of the above
|
||||||
|
licences without producing a new version of the EUPL, as long as they provide
|
||||||
|
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||||
|
Code from exclusive appropriation.
|
||||||
|
|
||||||
|
All other changes or additions to this Appendix require the production of a new
|
||||||
|
EUPL version.
|
||||||
82
README.md
Normal file
82
README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# `Geekeey.SemVer`
|
||||||
|
|
||||||
|
SemVer is a .NET library for parsing and comparing semantic version numbers. It provides a simple API for working with
|
||||||
|
semantic versioning, making it easy to manage version numbers in your .NET projects.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Parsing**: Parse semantic version strings into structured objects.
|
||||||
|
- **Comparison**: Compare semantic version objects to determine their order.
|
||||||
|
- **Validation**: Validate semantic version strings to ensure they conform to the SemVer specification.
|
||||||
|
- **Pre-release and Build Metadata**: Support for pre-release versions and build metadata as defined in the SemVer specification.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Install the NuGet package:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet add package Geekeey.SemVer
|
||||||
|
```
|
||||||
|
|
||||||
|
You may need to add our NuGet feed to your `nuget.config` this can be done by running the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet nuget add source -n geekeey https://code.geekeey.de/api/packages/geekeey/nuget/index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
var version = SemanticVersion.Parse("1.2.3-beta+build.7");
|
||||||
|
var next = new SemanticVersion(1, 3, 0);
|
||||||
|
|
||||||
|
Console.WriteLine(version);
|
||||||
|
Console.WriteLine(version.Major);
|
||||||
|
Console.WriteLine(version.Prerelease);
|
||||||
|
Console.WriteLine(version.Metadata);
|
||||||
|
Console.WriteLine(version < next);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `TryParse` when you need to handle invalid input without exceptions.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
if (SemanticVersion.TryParse("1.2.3-alpha", out var parsed))
|
||||||
|
{
|
||||||
|
Console.WriteLine(parsed);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Version ranges support npm-style and Maven-style syntax.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
var npmRange = SemanticVersionRange.Parse("^1.2.3");
|
||||||
|
var mavenRange = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
var candidate = SemanticVersion.Parse("1.4.2");
|
||||||
|
|
||||||
|
Console.WriteLine(npmRange.Contains(candidate));
|
||||||
|
Console.WriteLine(mavenRange.Contains(candidate));
|
||||||
|
Console.WriteLine(npmRange.ToString("m", null));
|
||||||
|
Console.WriteLine(mavenRange.ToString("ns", null));
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `SemanticVersion` and `SemanticVersionRange` integrate with `System.Text.Json`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Text.Json;
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
var version = SemanticVersion.Parse("1.2.3-beta+build.7");
|
||||||
|
var range = SemanticVersionRange.Parse("^1.2.3");
|
||||||
|
|
||||||
|
var versionJson = JsonSerializer.Serialize(version);
|
||||||
|
var rangeJson = JsonSerializer.Serialize(range);
|
||||||
|
|
||||||
|
var roundTrippedVersion = JsonSerializer.Deserialize<SemanticVersion>(versionJson);
|
||||||
|
var roundTrippedRange = JsonSerializer.Deserialize<SemanticVersionRange>(rangeJson);
|
||||||
|
```
|
||||||
11
global.json
Normal file
11
global.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/global.json",
|
||||||
|
"sdk": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"rollForward": "latestMinor"
|
||||||
|
},
|
||||||
|
"msbuild-sdks": {},
|
||||||
|
"test": {
|
||||||
|
"runner": "Microsoft.Testing.Platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
nuget.config
Normal file
19
nuget.config
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<config>
|
||||||
|
<add key="defaultPushSource" value="geekeey" />
|
||||||
|
</config>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="geekeey" value="https://code.geekeey.de/api/packages/geekeey/nuget/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
<packageSourceMapping>
|
||||||
|
<packageSource key="nuget">
|
||||||
|
<package pattern="*" />
|
||||||
|
</packageSource>
|
||||||
|
<packageSource key="geekeey">
|
||||||
|
<package pattern="Geekeey.*" />
|
||||||
|
</packageSource>
|
||||||
|
</packageSourceMapping>
|
||||||
|
</configuration>
|
||||||
4
semver.slnx
Normal file
4
semver.slnx
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<Solution>
|
||||||
|
<Project Path="src/semver/Geekeey.SemVer.csproj" />
|
||||||
|
<Project Path="src/semver.tests/Geekeey.SemVer.Tests.csproj" />
|
||||||
|
</Solution>
|
||||||
15
src/semver.tests/.editorconfig
Normal file
15
src/semver.tests/.editorconfig
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
[*.{cs,vb}]
|
||||||
|
# disable CA1822: Mark members as static
|
||||||
|
# -> TUnit requiring instance methods for test cases
|
||||||
|
dotnet_diagnostic.CA1822.severity = none
|
||||||
|
# disable CA1707: Identifiers should not contain underscores
|
||||||
|
dotnet_diagnostic.CA1707.severity = none
|
||||||
|
# disable IDE0060: Remove unused parameter
|
||||||
|
dotnet_diagnostic.IDE0060.severity = none
|
||||||
|
# disable IDE0005: Unnecessary using directive
|
||||||
|
dotnet_diagnostic.IDE0005.severity = none
|
||||||
|
# disable IDE0390: Method can be made synchronous
|
||||||
|
dotnet_diagnostic.IDE0390.severity = none
|
||||||
|
# disable IDE0391: Method can be made synchronous
|
||||||
|
dotnet_diagnostic.IDE0391.severity = none
|
||||||
21
src/semver.tests/Geekeey.SemVer.Tests.csproj
Normal file
21
src/semver.tests/Geekeey.SemVer.Tests.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="TUnit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\semver\Geekeey.SemVer.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
115
src/semver.tests/SemanticVersionComparerTests.cs
Normal file
115
src/semver.tests/SemanticVersionComparerTests.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer.Tests;
|
||||||
|
|
||||||
|
internal sealed class SemanticVersionComparerTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[Arguments("1.0.0", "2.0.0", -1)]
|
||||||
|
[Arguments("2.0.0", "1.0.0", 1)]
|
||||||
|
[Arguments("1.0.0", "1.1.0", -1)]
|
||||||
|
[Arguments("1.1.0", "1.0.0", 1)]
|
||||||
|
[Arguments("1.0.0", "1.0.1", -1)]
|
||||||
|
[Arguments("1.0.1", "1.0.0", 1)]
|
||||||
|
[Arguments("1.0.0-alpha", "1.0.0", -1)]
|
||||||
|
[Arguments("1.0.0", "1.0.0-alpha", 1)]
|
||||||
|
[Arguments("1.0.0-alpha", "1.0.0-alpha.1", -1)]
|
||||||
|
[Arguments("1.0.0-alpha.1", "1.0.0-alpha.beta", -1)]
|
||||||
|
[Arguments("1.0.0-alpha.beta", "1.0.0-beta", -1)]
|
||||||
|
[Arguments("1.0.0-beta", "1.0.0-beta.2", -1)]
|
||||||
|
[Arguments("1.0.0-beta.2", "1.0.0-beta.11", -1)]
|
||||||
|
[Arguments("1.0.0-beta.11", "1.0.0-rc.1", -1)]
|
||||||
|
[Arguments("1.0.0-rc.1", "1.0.0", -1)]
|
||||||
|
[Arguments("1.0.0-alpha.5", "1.0.0-alpha.10", -1)]
|
||||||
|
[Arguments("1.0.0-alpha.10", "1.0.0-alpha.5", 1)]
|
||||||
|
[Arguments("1.0.0-alpha.beta", "1.0.0-alpha.5", 1)]
|
||||||
|
[Arguments("1.0.0-alpha.5", "1.0.0-alpha.beta", -1)]
|
||||||
|
[Arguments("1.0.0-alpha.1.2", "1.0.0-alpha.1.2.3", -1)]
|
||||||
|
[Arguments("1.0.0-alpha.1.2.3", "1.0.0-alpha.1.2", 1)]
|
||||||
|
[Arguments("1.0.0-alpha-b", "1.0.0-alpha-a", 1)]
|
||||||
|
[Arguments("1.0.0-0.3.7", "1.0.0-x.7.z.92", -1)]
|
||||||
|
[Arguments("1.0.0-x.7.z.92", "1.0.0-0.3.7", 1)]
|
||||||
|
[Arguments("1.0.0+build.1", "1.0.0+build.2", 0)]
|
||||||
|
[Arguments("1.0.0-alpha+build.1", "1.0.0-alpha+build.2", 0)]
|
||||||
|
public async Task I_can_compare_by_precedence(string v1Str, string v2Str, int expected)
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse(v1Str);
|
||||||
|
var v2 = SemanticVersion.Parse(v2Str);
|
||||||
|
var result = SemanticVersionComparer.Priority.Compare(v1, v2);
|
||||||
|
await Assert.That(Math.Sign(result)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("1.0.0+build.1", "1.0.0+build.2", 0)]
|
||||||
|
[Arguments("1.0.0-alpha+build.1", "1.0.0-alpha+build.2", 0)]
|
||||||
|
public async Task I_can_confirm_precedence_ignores_metadata(string v1Str, string v2Str, int expected)
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse(v1Str);
|
||||||
|
var v2 = SemanticVersion.Parse(v2Str);
|
||||||
|
var result = SemanticVersionComparer.Priority.Compare(v1, v2);
|
||||||
|
await Assert.That(result).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("1.0.0", "1.0.0+build.1", -1)]
|
||||||
|
[Arguments("1.0.0+build.1", "1.0.0", 1)]
|
||||||
|
[Arguments("1.0.0+build.1", "1.0.0+build.2", -1)]
|
||||||
|
[Arguments("1.0.0+build.1.1", "1.0.0+build.1", 1)]
|
||||||
|
[Arguments("1.0.0+a.b", "1.0.0+a.a", 1)]
|
||||||
|
[Arguments("1.0.0+a.a", "1.0.0+a.b", -1)]
|
||||||
|
[Arguments("1.0.0+a.b", "1.0.0+a.b.c", -1)]
|
||||||
|
[Arguments("1.0.0+a.b.c", "1.0.0+a.b", 1)]
|
||||||
|
[Arguments("1.0.0+1.2.3", "1.0.0+1.2.4", -1)]
|
||||||
|
[Arguments("1.0.0+1.2.4", "1.0.0+1.2.3", 1)]
|
||||||
|
[Arguments("1.0.0+01", "1.0.0+1", -1)]
|
||||||
|
public async Task I_can_compare_by_sort_order(string v1Str, string v2Str, int expected)
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse(v1Str);
|
||||||
|
var v2 = SemanticVersion.Parse(v2Str);
|
||||||
|
var result = SemanticVersionComparer.SortOrder.Compare(v1, v2);
|
||||||
|
await Assert.That(Math.Sign(result)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_confirm_precedence_equality_ignores_metadata()
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse("1.0.0-alpha+build.1");
|
||||||
|
var v2 = SemanticVersion.Parse("1.0.0-alpha+build.2");
|
||||||
|
|
||||||
|
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsTrue();
|
||||||
|
await Assert.That(SemanticVersionComparer.Priority.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.Priority.GetHashCode(v2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_confirm_sort_order_equality_includes_metadata()
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse("1.0.0-alpha+build.1");
|
||||||
|
var v2 = SemanticVersion.Parse("1.0.0-alpha+build.2");
|
||||||
|
|
||||||
|
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsFalse();
|
||||||
|
await Assert.That(SemanticVersionComparer.SortOrder.GetHashCode(v1)).IsNotEqualTo(SemanticVersionComparer.SortOrder.GetHashCode(v2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_confirm_equality_for_identical_versions()
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse("1.0.0-alpha.1");
|
||||||
|
var v2 = SemanticVersion.Parse("1.0.0-alpha.1");
|
||||||
|
|
||||||
|
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsTrue();
|
||||||
|
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsTrue();
|
||||||
|
await Assert.That(SemanticVersionComparer.Priority.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.Priority.GetHashCode(v2));
|
||||||
|
await Assert.That(SemanticVersionComparer.SortOrder.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.SortOrder.GetHashCode(v2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_confirm_equality_for_different_versions()
|
||||||
|
{
|
||||||
|
var v1 = SemanticVersion.Parse("1.0.0-alpha.1");
|
||||||
|
var v2 = SemanticVersion.Parse("1.0.0-alpha.2");
|
||||||
|
|
||||||
|
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsFalse();
|
||||||
|
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/semver.tests/SemanticVersionRangeTests.cs
Normal file
261
src/semver.tests/SemanticVersionRangeTests.cs
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer.Tests;
|
||||||
|
|
||||||
|
internal sealed class SemanticVersionRangeTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[Arguments("1.2.3", "1.2.3", true)]
|
||||||
|
[Arguments("1.2.3", "1.2.4", false)]
|
||||||
|
[Arguments(">1.2.3", "1.2.4", true)]
|
||||||
|
[Arguments(">1.2.3", "1.2.3", false)]
|
||||||
|
[Arguments(">=1.2.3", "1.2.3", true)]
|
||||||
|
[Arguments("<1.2.3", "1.2.2", true)]
|
||||||
|
[Arguments("<=1.2.3", "1.2.3", true)]
|
||||||
|
public async Task I_can_satisfy_npm_basic_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("1.2.3 - 2.3.4", "1.2.3", true)]
|
||||||
|
[Arguments("1.2.3 - 2.3.4", "2.3.4", true)]
|
||||||
|
[Arguments("1.2.3 - 2.3.4", "1.2.2", false)]
|
||||||
|
[Arguments("1.2.3 - 2.3.4", "2.3.5", false)]
|
||||||
|
public async Task I_can_satisfy_npm_hyphen_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("^1.2.3", "1.2.3", true)]
|
||||||
|
[Arguments("^1.2.3", "1.9.9", true)]
|
||||||
|
[Arguments("^1.2.3", "2.0.0", false)]
|
||||||
|
[Arguments("^0.2.3", "0.2.3", true)]
|
||||||
|
[Arguments("^0.2.3", "0.2.4", true)]
|
||||||
|
[Arguments("^0.2.3", "0.3.0", false)]
|
||||||
|
[Arguments("^0.0.3", "0.0.3", true)]
|
||||||
|
[Arguments("^0.0.3", "0.0.4", false)]
|
||||||
|
public async Task I_can_satisfy_npm_caret_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("~1.2.3", "1.2.3", true)]
|
||||||
|
[Arguments("~1.2.3", "1.2.9", true)]
|
||||||
|
[Arguments("~1.2.3", "1.3.0", false)]
|
||||||
|
[Arguments("~1.2", "1.2.0", true)]
|
||||||
|
[Arguments("~1.2", "1.2.9", true)]
|
||||||
|
[Arguments("~1.2", "1.3.0", false)]
|
||||||
|
[Arguments("~1", "1.0.0", true)]
|
||||||
|
[Arguments("~1", "1.9.9", true)]
|
||||||
|
[Arguments("~1", "2.0.0", false)]
|
||||||
|
public async Task I_can_satisfy_npm_tilde_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("1.2.x", "1.2.0", true)]
|
||||||
|
[Arguments("1.2.x", "1.2.9", true)]
|
||||||
|
[Arguments("1.2.x", "1.3.0", false)]
|
||||||
|
[Arguments("1.x", "1.0.0", true)]
|
||||||
|
[Arguments("1.x", "1.9.9", true)]
|
||||||
|
[Arguments("1.x", "2.0.0", false)]
|
||||||
|
[Arguments("*", "0.0.0", true)]
|
||||||
|
[Arguments("*", "9.9.9", true)]
|
||||||
|
public async Task I_can_satisfy_npm_wildcard_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_parse_exact_version_ranges_via_public_api()
|
||||||
|
{
|
||||||
|
var range = SemanticVersionRange.Parse("=1.2.3");
|
||||||
|
|
||||||
|
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.3"))).IsTrue();
|
||||||
|
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse();
|
||||||
|
await Assert.That(range.ToString("n", null)).IsEqualTo("1.2.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(">1.2.3 || <1.0.0", "1.2.4", true)]
|
||||||
|
[Arguments(">1.2.3 || <1.0.0", "0.9.9", true)]
|
||||||
|
[Arguments(">1.2.3 || <1.0.0", "1.1.0", false)]
|
||||||
|
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "2.0.0", true)]
|
||||||
|
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "5.0.0", true)]
|
||||||
|
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "7.0.0", false)]
|
||||||
|
public async Task I_can_satisfy_npm_or_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("^6.2.3-alpha.1", "6.2.3-alpha.1", true)]
|
||||||
|
[Arguments("^6.2.3-alpha.1", "6.2.3-beta", true)]
|
||||||
|
[Arguments("^6.2.3-alpha.1", "6.2.2", false)]
|
||||||
|
[Arguments("^6.2.3-alpha.1", "7.0.0", false)]
|
||||||
|
public async Task I_can_satisfy_complex_npm_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_handle_two_constraints_without_combining_them()
|
||||||
|
{
|
||||||
|
var range = SemanticVersionRange.Parse(">=1.0.0 !=2.0.0");
|
||||||
|
|
||||||
|
await Assert.That(range.Contains(SemanticVersion.Parse("1.5.0"))).IsTrue();
|
||||||
|
await Assert.That(range.Contains(SemanticVersion.Parse("2.0.0"))).IsFalse();
|
||||||
|
await Assert.That(range.ToString("n", null)).IsEqualTo(">=1.0.0 !=2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("^5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||||
|
[Arguments("5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||||
|
[Arguments(">=5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||||
|
[Arguments(">5.*", "5.0.1", "5.0.0", ">5.0.0 <6.0.0")]
|
||||||
|
[Arguments("<=5.*", "5.9.9", "6.0.0", "<6.0.0")]
|
||||||
|
[Arguments("<5.*", "4.9.9", "5.0.0", "<5.0.0")]
|
||||||
|
public async Task I_can_handle_ranges_with_wildcard(string range, string inside, string outside, string expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
|
||||||
|
await Assert.That(r.Contains(SemanticVersion.Parse(inside))).IsTrue();
|
||||||
|
await Assert.That(r.Contains(SemanticVersion.Parse(outside))).IsFalse();
|
||||||
|
await Assert.That(r.ToString("n", null)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("[1.2.3]", "1.2.3", true)]
|
||||||
|
[Arguments("[1.2.3]", "1.2.4", false)]
|
||||||
|
[Arguments("[1.2.3, 1.4.0)", "1.2.3", true)]
|
||||||
|
[Arguments("[1.2.3, 1.4.0)", "1.3.9", true)]
|
||||||
|
[Arguments("[1.2.3, 1.4.0)", "1.4.0", false)]
|
||||||
|
[Arguments("(1.2.3, 1.4.0]", "1.2.3", false)]
|
||||||
|
[Arguments("(1.2.3, 1.4.0]", "1.4.0", true)]
|
||||||
|
[Arguments("[1.2.3,)", "1.2.3", true)]
|
||||||
|
[Arguments("[1.2.3,)", "9.9.9", true)]
|
||||||
|
[Arguments("(,1.4.0]", "1.4.0", true)]
|
||||||
|
[Arguments("(,1.4.0]", "0.0.0", true)]
|
||||||
|
[Arguments("(,)", "0.0.0", true)]
|
||||||
|
[Arguments("(,)", "9.9.9", true)]
|
||||||
|
public async Task I_can_satisfy_maven_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("[1.2,1.3],[1.5,)", "1.2.0", true)]
|
||||||
|
[Arguments("[1.2,1.3],[1.5,)", "1.4.0", false)]
|
||||||
|
[Arguments("[1.2,1.3],[1.5,)", "1.5.0", true)]
|
||||||
|
public async Task I_can_satisfy_maven_or_ranges(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("^1.2.3", "1.2.3-alpha", false)]
|
||||||
|
[Arguments("^1.2.3-alpha", "1.2.3-beta", true)]
|
||||||
|
[Arguments("^1.2.3-alpha", "1.2.4", true)]
|
||||||
|
[Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
|
||||||
|
public async Task I_can_satisfy_prerelease_rules(string range, string version, bool expected)
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
var v = SemanticVersion.Parse(version);
|
||||||
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(">=1.0.0 !2.0.0")]
|
||||||
|
[Arguments("1.2.3 - ")]
|
||||||
|
[Arguments("!=1.x")]
|
||||||
|
public async Task I_can_not_parse_invalid_ranges(string range)
|
||||||
|
{
|
||||||
|
await Assert.That(() => SemanticVersionRange.Parse(range))
|
||||||
|
.Throws<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_serialize_to_json()
|
||||||
|
{
|
||||||
|
var r = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(r);
|
||||||
|
await Assert.That(json).IsEqualTo("\"^1.2.3\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_deserialize_from_json()
|
||||||
|
{
|
||||||
|
var json = "\"^1.2.3\"";
|
||||||
|
var r = System.Text.Json.JsonSerializer.Deserialize<SemanticVersionRange>(json);
|
||||||
|
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
|
||||||
|
await Assert.That(r.Contains(new SemanticVersion(1, 2, 4))).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_use_npm_short_format_by_default()
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
await Assert.That(value.ToString()).IsEqualTo("^1.2.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("^1.2.3", "m", "[1.2.3,2.0.0)")]
|
||||||
|
[Arguments("~1.2.3", "m", "[1.2.3,1.3.0)")]
|
||||||
|
[Arguments("1.2.3", "m", "[1.2.3]")]
|
||||||
|
[Arguments("[1.2.3,2.0.0)", "n", ">=1.2.3 <2.0.0")]
|
||||||
|
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
|
||||||
|
[Arguments("[1.2.3,1.3.0)", "ns", "~1.2.3")]
|
||||||
|
[Arguments("[1.2,1.3],[1.5,)", "n", ">=1.2.0 <=1.3.0 || >=1.5.0")]
|
||||||
|
[Arguments("*", "m", "(,)")]
|
||||||
|
[Arguments("(,)", "ns", "*")]
|
||||||
|
public async Task I_can_convert_range_formats(string range, string format, string expected)
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse(range);
|
||||||
|
await Assert.That(value.ToString(format, null)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_format_ranges_to_chars()
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
var destination = new char[32];
|
||||||
|
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
||||||
|
|
||||||
|
await Assert.That(success).IsTrue();
|
||||||
|
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
var destination = new char[5];
|
||||||
|
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
||||||
|
|
||||||
|
await Assert.That(success).IsFalse();
|
||||||
|
await Assert.That(charsWritten).IsEqualTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/semver.tests/SemanticVersionTests.cs
Normal file
225
src/semver.tests/SemanticVersionTests.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer.Tests;
|
||||||
|
|
||||||
|
internal sealed class SemanticVersionTests
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions RelaxedOptions = new()
|
||||||
|
{
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(0, 0, 0, 0)]
|
||||||
|
[Arguments(1, 1, 1, 0)]
|
||||||
|
[Arguments(1, 2, 0, 0)]
|
||||||
|
[Arguments(45, 2, 4, 0)]
|
||||||
|
public async Task I_can_create_semver_from_version(int major, int minor, int build, int revision)
|
||||||
|
{
|
||||||
|
var version = new Version(major, minor, build, revision);
|
||||||
|
var v = new SemanticVersion(version);
|
||||||
|
|
||||||
|
await Assert.That(v.Major).IsEqualTo((ulong)major);
|
||||||
|
await Assert.That(v.Minor).IsEqualTo((ulong)minor);
|
||||||
|
await Assert.That(v.Patch).IsEqualTo((ulong)build);
|
||||||
|
await Assert.That(v.Prerelease).IsNull();
|
||||||
|
await Assert.That(v.Metadata).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(1UL)]
|
||||||
|
[Arguments(0UL)]
|
||||||
|
public async Task I_can_create_semver_with_major(ulong major)
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(major);
|
||||||
|
|
||||||
|
await Assert.That(v.Major).IsEqualTo(major);
|
||||||
|
await Assert.That(v.Minor).IsEqualTo(0UL);
|
||||||
|
await Assert.That(v.Patch).IsEqualTo(0UL);
|
||||||
|
await Assert.That(v.Prerelease).IsNull();
|
||||||
|
await Assert.That(v.Metadata).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(1UL, 2UL)]
|
||||||
|
public async Task I_can_create_semver_with_major_and_minor(ulong major, ulong minor)
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(major, minor);
|
||||||
|
|
||||||
|
await Assert.That(v.Major).IsEqualTo(major);
|
||||||
|
await Assert.That(v.Minor).IsEqualTo(minor);
|
||||||
|
await Assert.That(v.Patch).IsEqualTo(0UL);
|
||||||
|
await Assert.That(v.Prerelease).IsNull();
|
||||||
|
await Assert.That(v.Metadata).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(1UL, 2UL, 3UL)]
|
||||||
|
public async Task I_can_create_semver_with_major_minor_and_patch(ulong major, ulong minor, ulong patch)
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(major, minor, patch);
|
||||||
|
|
||||||
|
await Assert.That(v.Major).IsEqualTo(major);
|
||||||
|
await Assert.That(v.Minor).IsEqualTo(minor);
|
||||||
|
await Assert.That(v.Patch).IsEqualTo(patch);
|
||||||
|
await Assert.That(v.Prerelease).IsNull();
|
||||||
|
await Assert.That(v.Metadata).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(1UL, 2UL, 3UL, "alpha", "build.1")]
|
||||||
|
[Arguments(1UL, 2UL, 3UL, null, null)]
|
||||||
|
public async Task I_can_create_semver_with_all_components(ulong major, ulong minor, ulong patch, string? prerelease, string? metadata)
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(major, minor, patch, prerelease, metadata);
|
||||||
|
|
||||||
|
await Assert.That(v.Major).IsEqualTo(major);
|
||||||
|
await Assert.That(v.Minor).IsEqualTo(minor);
|
||||||
|
await Assert.That(v.Patch).IsEqualTo(patch);
|
||||||
|
await Assert.That(v.Prerelease).IsEqualTo(prerelease);
|
||||||
|
await Assert.That(v.Metadata).IsEqualTo(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_compare_identical_versions()
|
||||||
|
{
|
||||||
|
var v1 = new SemanticVersion(1, 2, 3);
|
||||||
|
var v2 = new SemanticVersion(1, 2, 3);
|
||||||
|
|
||||||
|
await Assert.That(v1.CompareTo(v2)).IsEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_compare_with_objects()
|
||||||
|
{
|
||||||
|
var v1 = new SemanticVersion(1, 2, 3);
|
||||||
|
object v2 = new SemanticVersion(1, 2, 3);
|
||||||
|
|
||||||
|
await Assert.That(v1.CompareTo(v2)).IsEqualTo(0);
|
||||||
|
await Assert.That(v1.CompareTo(null)).IsEqualTo(1);
|
||||||
|
await Assert.That(v1.CompareTo("not a version")).IsEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_confirm_operators_work()
|
||||||
|
{
|
||||||
|
var v1 = new SemanticVersion(1, 2, 3);
|
||||||
|
var v2 = new SemanticVersion(2, 0, 0);
|
||||||
|
|
||||||
|
await Assert.That(v1 < v2).IsTrue();
|
||||||
|
await Assert.That(v1 <= v2).IsTrue();
|
||||||
|
await Assert.That(v1 > v2).IsFalse();
|
||||||
|
await Assert.That(v1 >= v2).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_parse_from_string()
|
||||||
|
{
|
||||||
|
var result = SemanticVersion.Parse("1.2.3");
|
||||||
|
await Assert.That(result.Major).IsEqualTo(1UL);
|
||||||
|
await Assert.That(result.Minor).IsEqualTo(2UL);
|
||||||
|
await Assert.That(result.Patch).IsEqualTo(3UL);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_try_parse_from_string()
|
||||||
|
{
|
||||||
|
var success = SemanticVersion.TryParse("1.2.3-alpha+build.1", out var result);
|
||||||
|
await Assert.That(success).IsTrue();
|
||||||
|
await Assert.That(result.Major).IsEqualTo(1UL);
|
||||||
|
await Assert.That(result.Minor).IsEqualTo(2UL);
|
||||||
|
await Assert.That(result.Patch).IsEqualTo(3UL);
|
||||||
|
await Assert.That(result.Prerelease).IsEqualTo("alpha");
|
||||||
|
await Assert.That(result.Metadata).IsEqualTo("build.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(1, 2, 3, null, null, "1.2.3")]
|
||||||
|
[Arguments(1, 2, 3, "alpha", null, "1.2.3-alpha")]
|
||||||
|
[Arguments(1, 2, 3, "alpha", "build.1", "1.2.3-alpha+build.1")]
|
||||||
|
public async Task I_can_get_string_representation(ulong major, ulong minor, ulong patch, string? pre, string? meta, string expected)
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(major, minor, patch, pre, meta);
|
||||||
|
// Note: format "f" is needed for full version including metadata based on code
|
||||||
|
var format = string.IsNullOrEmpty(meta) ? (string.IsNullOrEmpty(pre) ? null : "s") : "f";
|
||||||
|
|
||||||
|
await Assert.That(v.ToString(format, null)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_format_to_chars()
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(1, 2, 3, "beta", "456");
|
||||||
|
var dest = new char[32];
|
||||||
|
var success = v.TryFormat(dest, out var charsWritten, "f", null);
|
||||||
|
|
||||||
|
await Assert.That(success).IsTrue();
|
||||||
|
await Assert.That(new string(dest[..charsWritten])).IsEqualTo("1.2.3-beta+456");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(1, 2, 3, null, null, "\"1.2.3\"")]
|
||||||
|
[Arguments(1, 2, 3, "alpha", null, "\"1.2.3-alpha\"")]
|
||||||
|
[Arguments(1, 2, 3, "alpha", "build.1", "\"1.2.3-alpha+build.1\"")]
|
||||||
|
public async Task I_can_serialize_to_json(ulong major, ulong minor, ulong patch, string? pre, string? meta, string expectedJson)
|
||||||
|
{
|
||||||
|
var v = new SemanticVersion(major, minor, patch, pre, meta);
|
||||||
|
var json = JsonSerializer.Serialize(v, RelaxedOptions);
|
||||||
|
|
||||||
|
await Assert.That(json).IsEqualTo(expectedJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_deserialize_from_json()
|
||||||
|
{
|
||||||
|
var json = "\"1.2.3-beta+789\"";
|
||||||
|
var v = JsonSerializer.Deserialize<SemanticVersion>(json);
|
||||||
|
|
||||||
|
await Assert.That(v.Major).IsEqualTo(1UL);
|
||||||
|
await Assert.That(v.Minor).IsEqualTo(2UL);
|
||||||
|
await Assert.That(v.Patch).IsEqualTo(3UL);
|
||||||
|
await Assert.That(v.Prerelease).IsEqualTo("beta");
|
||||||
|
await Assert.That(v.Metadata).IsEqualTo("789");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_handle_invalid_json_token()
|
||||||
|
{
|
||||||
|
var json = "123"; // Number instead of string
|
||||||
|
|
||||||
|
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
|
||||||
|
.Throws<JsonException>()
|
||||||
|
.WithMessage("Expected string");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_serialize_as_part_of_object()
|
||||||
|
{
|
||||||
|
var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") };
|
||||||
|
var json = JsonSerializer.Serialize(obj, RelaxedOptions);
|
||||||
|
|
||||||
|
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ "{\"Version\":\"1.0.0-rc.1+metadata\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_deserialize_null()
|
||||||
|
{
|
||||||
|
var json = "null";
|
||||||
|
var v = JsonSerializer.Deserialize<SemanticVersion>(json);
|
||||||
|
|
||||||
|
await Assert.That(v).IsEqualTo(default(SemanticVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_deserialize_empty_string()
|
||||||
|
{
|
||||||
|
var json = "\"\"";
|
||||||
|
|
||||||
|
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
|
||||||
|
.Throws<JsonException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/semver/Geekeey.SemVer.csproj
Normal file
31
src/semver/Geekeey.SemVer.csproj
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<InternalsVisibleTo Include="Geekeey.SemVer.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageReadmeFile>package-readme.md</PackageReadmeFile>
|
||||||
|
<PackageDescription>A .NET library for parsing, comparing, formatting, and serializing semantic versions and version ranges.</PackageDescription>
|
||||||
|
<PackageIcon>package-icon.png</PackageIcon>
|
||||||
|
<PackageProjectUrl>https://code.geekeey.de/geekeey/semver/src/branch/main/src/semver</PackageProjectUrl>
|
||||||
|
<PackageLicenseExpression>EUPL-1.2</PackageLicenseExpression>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include=".\package-icon.png" Pack="true" PackagePath="\" Visible="false" />
|
||||||
|
<None Include=".\package-readme.md" Pack="true" PackagePath="\" Visible="false" />
|
||||||
|
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
51
src/semver/SemanticVersion.Comparison.cs
Normal file
51
src/semver/SemanticVersion.Comparison.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersion : IComparable, IComparable<SemanticVersion>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CompareTo(object? obj)
|
||||||
|
{
|
||||||
|
return obj is not SemanticVersion other ? 1 : CompareTo(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CompareTo(SemanticVersion other)
|
||||||
|
{
|
||||||
|
return SemanticVersionComparer.Priority.Compare(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is less than another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator <(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is less than or equal to another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator <=(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is greater than another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator >(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is greater than or equal to another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator >=(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/semver/SemanticVersion.Formatting.cs
Normal file
112
src/semver/SemanticVersion.Formatting.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersion : ISpanFormattable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return ToString(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IFormattable
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <remarks>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>s - Default SemVer - [1.2.3-beta.4]</description></item>
|
||||||
|
/// <item><description>f - Full SemVer - [1.2.3-beta.4+5]</description></item>
|
||||||
|
/// <item><description>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||||
|
{
|
||||||
|
if (format is not null and not "s" and not "f" and not "r")
|
||||||
|
{
|
||||||
|
throw new FormatException($"The format string '{format}' is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||||
|
handler.AppendFormatted(this, format);
|
||||||
|
return handler.ToStringAndClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISpanFormattable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to format the semantic version into the specified span of characters.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryFormat(Span{char},out int,ReadOnlySpan{char},IFormatProvider)">ISpanFormattable.TryFormat</see>
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten)
|
||||||
|
{
|
||||||
|
return TryFormat(destination, out charsWritten, default, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <remarks>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>s - Default SemVer - [1.2.3-beta.4]</description></item>
|
||||||
|
/// <item><description>f - Full SemVer - [1.2.3-beta.4+5]</description></item>
|
||||||
|
/// <item><description>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
|
||||||
|
if (format.IsEmpty)
|
||||||
|
{
|
||||||
|
format = "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format is not "s" and not "f" and not "r")
|
||||||
|
{
|
||||||
|
throw new FormatException($"The format string '{format}' is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!destination.TryWrite($"{Major}.{Minor}.{Patch}", out var written))
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = destination[written..];
|
||||||
|
charsWritten += written;
|
||||||
|
|
||||||
|
if (Prerelease is { Length: > 0 } && format is "s" or "f")
|
||||||
|
{
|
||||||
|
if (!destination.TryWrite(provider, $"-{Prerelease}", out written))
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = destination[written..];
|
||||||
|
charsWritten += written;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Metadata is { Length: > 0 } && format is "f")
|
||||||
|
{
|
||||||
|
if (!destination.TryWrite(provider, $"+{Metadata}", out written))
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = destination[written..];
|
||||||
|
charsWritten += written;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = destination;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
41
src/semver/SemanticVersion.JsonConverter.cs
Normal file
41
src/semver/SemanticVersion.JsonConverter.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(SemanticVersionJsonConverter))]
|
||||||
|
public readonly partial record struct SemanticVersion
|
||||||
|
{
|
||||||
|
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
|
||||||
|
{
|
||||||
|
public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType is JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
|
||||||
|
{
|
||||||
|
throw new JsonException("Expected string");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Parse(value);
|
||||||
|
}
|
||||||
|
catch (FormatException exception)
|
||||||
|
{
|
||||||
|
throw new JsonException(exception.Message, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString("f", null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/semver/SemanticVersion.Parsing.cs
Normal file
155
src/semver/SemanticVersion.Parsing.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVersion>
|
||||||
|
{
|
||||||
|
#region IParsable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a string into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||||
|
public static SemanticVersion Parse(string s)
|
||||||
|
{
|
||||||
|
return Parse(s.AsSpan(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static SemanticVersion Parse(string s, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
return Parse(s.AsSpan(), provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a string into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersion)"/>
|
||||||
|
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParse(s, null, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParse(s.AsSpan(), provider, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISpanParsable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a span of characters into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||||
|
public static SemanticVersion Parse(ReadOnlySpan<char> s)
|
||||||
|
{
|
||||||
|
return Parse(s, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static SemanticVersion Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
if (!TryParse(s, provider, out var result))
|
||||||
|
{
|
||||||
|
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a span of characters into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersion)"/>
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParse(s, null, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParsePartially(s, out result, out var components) && components is 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// components = 0: wildcard at major
|
||||||
|
// components = 1: wildcard at minor
|
||||||
|
// components = 2: wildcard at patch
|
||||||
|
// components = 3: no wildcards
|
||||||
|
internal static bool TryParsePartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
||||||
|
{
|
||||||
|
version = default;
|
||||||
|
components = 0;
|
||||||
|
|
||||||
|
if (s.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = default(string);
|
||||||
|
if (s.IndexOf('+') is >= 0 and var metadataIndex)
|
||||||
|
{
|
||||||
|
if (s[(metadataIndex + 1)..] is not { IsEmpty: false } value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = new string(value);
|
||||||
|
s = s[..metadataIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
var prerelease = default(string);
|
||||||
|
if (s.IndexOf('-') is >= 0 and var prereleaseIndex)
|
||||||
|
{
|
||||||
|
if (s[(prereleaseIndex + 1)..] is not { IsEmpty: false } value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
prerelease = new string(value);
|
||||||
|
s = s[..prereleaseIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<Range> destination = stackalloc Range[3];
|
||||||
|
Span<ulong> component = stackalloc ulong[3];
|
||||||
|
|
||||||
|
foreach (var range in destination[..s.Split(destination, '.')])
|
||||||
|
{
|
||||||
|
if (s[range] is not { IsEmpty: false } segment)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment is "*" or "x" or "X")
|
||||||
|
{
|
||||||
|
version = components switch
|
||||||
|
{
|
||||||
|
0 => default,
|
||||||
|
1 => new SemanticVersion(component[0], 0, 0),
|
||||||
|
2 => new SemanticVersion(component[0], component[1], 0),
|
||||||
|
_ => throw new InvalidOperationException(),
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ulong.TryParse(segment, NumberStyles.None, CultureInfo.InvariantCulture, out var value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
component[components++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
version = new SemanticVersion(component[0], component[1], component[2], prerelease, metadata);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/semver/SemanticVersion.cs
Normal file
108
src/semver/SemanticVersion.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
|
||||||
|
/// </summary>
|
||||||
|
public readonly partial record struct SemanticVersion
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a <see cref="Version"/> into the equivalent semantic version.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="version">The version to be converted to a semantic version.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="Version"/> numbers have the form <em>major</em>.<em>minor</em>[.<em>build</em>[.<em>revision</em>]]
|
||||||
|
/// where square brackets ('[' and ']') indicate optional components. The first three parts are converted to the
|
||||||
|
/// major, minor, and patch version numbers of a semantic version.
|
||||||
|
/// </remarks>
|
||||||
|
public SemanticVersion(Version version)
|
||||||
|
: this((ulong)version.Major, (ulong)version.Minor, (ulong)version.Build)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="major">The major version number.</param>
|
||||||
|
public SemanticVersion(ulong major)
|
||||||
|
: this(major, 0, 0, default, default)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="major">The major version number.</param>
|
||||||
|
/// <param name="minor">The minor version number.</param>
|
||||||
|
public SemanticVersion(ulong major, ulong minor)
|
||||||
|
: this(major, minor, 0, default, default)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="major">The major version number.</param>
|
||||||
|
/// <param name="minor">The minor version number.</param>
|
||||||
|
/// <param name="patch">The patch version number.</param>
|
||||||
|
public SemanticVersion(ulong major, ulong minor, ulong patch)
|
||||||
|
: this(major, minor, patch, default, default)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="major">The major version number.</param>
|
||||||
|
/// <param name="minor">The minor version number.</param>
|
||||||
|
/// <param name="patch">The patch version number.</param>
|
||||||
|
/// <param name="prerelease">The prerelease identifiers.</param>
|
||||||
|
/// <param name="metadata">The build metadata identifiers.</param>
|
||||||
|
public SemanticVersion(ulong major, ulong minor, ulong patch, string? prerelease, string? metadata)
|
||||||
|
{
|
||||||
|
Major = major;
|
||||||
|
Minor = minor;
|
||||||
|
Patch = patch;
|
||||||
|
Prerelease = prerelease;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the major version number of the semantic version. This value is a non-negative integer
|
||||||
|
/// representing the most significant component of the version, typically incremented when
|
||||||
|
/// making incompatible API changes.
|
||||||
|
/// </summary>
|
||||||
|
public ulong Major { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minor version number of the semantic version. This value is a non-negative integer
|
||||||
|
/// representing the second most significant component of the version, typically incremented
|
||||||
|
/// when adding backward-compatible functionalities.
|
||||||
|
/// </summary>
|
||||||
|
public ulong Minor { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the patch version number of the semantic version. This value is a non-negative integer
|
||||||
|
/// representing the least significant component of the version, typically incremented for backwards-compatible
|
||||||
|
/// bug fixes or other small changes.
|
||||||
|
/// </summary>
|
||||||
|
public ulong Patch { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the prerelease label of the semantic version. This value is an optional string
|
||||||
|
/// indicating a version that is still in development or not yet considered stable.
|
||||||
|
/// Examples of prerelease labels include "alpha", "beta", or "rc".
|
||||||
|
/// When present, the prerelease label is used to differentiate between versions with
|
||||||
|
/// the same major, minor, and patch numbers.
|
||||||
|
/// </summary>
|
||||||
|
public string? Prerelease { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the build metadata associated with the semantic version. This optional value provides
|
||||||
|
/// additional information about the build, such as build number, commit hash, or other relevant
|
||||||
|
/// details. It does not affect the precedence of the version.
|
||||||
|
/// </summary>
|
||||||
|
public string? Metadata { get; }
|
||||||
|
}
|
||||||
307
src/semver/SemanticVersionComparer.cs
Normal file
307
src/semver/SemanticVersionComparer.cs
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides comparers for <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SemanticVersionComparer : IEqualityComparer<SemanticVersion?>, IEqualityComparer, IComparer<SemanticVersion?>, IComparer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the comparer that determines the precedence of semantic versions.
|
||||||
|
/// </summary>
|
||||||
|
public static SemanticVersionComparer Priority { get; } = new PrecedenceSemanticVersionComparer();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the comparer that determines the sort order of semantic versions.
|
||||||
|
/// </summary>
|
||||||
|
public static SemanticVersionComparer SortOrder { get; } = new SortOrderSemanticVersionComparer();
|
||||||
|
|
||||||
|
bool IEqualityComparer.Equals(object? x, object? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null || y is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is SemanticVersion v1 && y is SemanticVersion v2)
|
||||||
|
{
|
||||||
|
return Equals(v1, v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"Type of argument is not {nameof(SemanticVersion)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract bool Equals(SemanticVersion? x, SemanticVersion? y);
|
||||||
|
|
||||||
|
int IEqualityComparer.GetHashCode(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj is SemanticVersion v)
|
||||||
|
{
|
||||||
|
return GetHashCode(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"Type of argument is not {nameof(SemanticVersion)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract int GetHashCode(SemanticVersion? v);
|
||||||
|
|
||||||
|
int IComparer.Compare(object? x, object? y)
|
||||||
|
{
|
||||||
|
if (x is null && y is null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y is null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is SemanticVersion v1 && y is SemanticVersion v2)
|
||||||
|
{
|
||||||
|
return Compare(v1, v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"Type of argument is not {nameof(SemanticVersion)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract int Compare(SemanticVersion? x, SemanticVersion? y);
|
||||||
|
|
||||||
|
/// <para>Precedence order is determined by comparing the major, minor, patch, and prerelease portion in order from left to right. Versions that differ only by build metadata have the same precedence. The major, minor, and patch version numbers are compared numerically. A prerelease version precedes a release version.</para>
|
||||||
|
/// <para>The prerelease portion is compared by comparing each prerelease identifier from left to right. Numeric prerelease identifiers precede alphanumeric identifiers. Numeric identifiers are compared numerically. Alphanumeric identifiers are compared lexically in ASCII sort order. A longer series of prerelease identifiers follows a shorter series if all the preceding identifiers are equal.</para>
|
||||||
|
private sealed class PrecedenceSemanticVersionComparer : SemanticVersionComparer
|
||||||
|
{
|
||||||
|
public override bool Equals(SemanticVersion? x, SemanticVersion? y)
|
||||||
|
{
|
||||||
|
if (x is null && y is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null || y is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v1 = x.Value;
|
||||||
|
var v2 = y.Value;
|
||||||
|
|
||||||
|
return v1.Major == v2.Major &&
|
||||||
|
v1.Minor == v2.Minor &&
|
||||||
|
v1.Patch == v2.Patch &&
|
||||||
|
string.Equals(v1.Prerelease, v2.Prerelease, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode(SemanticVersion? v)
|
||||||
|
{
|
||||||
|
if (v is null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ver = v.Value;
|
||||||
|
return HashCode.Combine(ver.Major, ver.Minor, ver.Patch, ver.Prerelease);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Compare(SemanticVersion? x, SemanticVersion? y)
|
||||||
|
{
|
||||||
|
if (x is null && y is null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y is null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v1 = x.Value;
|
||||||
|
var v2 = y.Value;
|
||||||
|
|
||||||
|
var cmp = v1.Major.CompareTo(v2.Major);
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp = v1.Minor.CompareTo(v2.Minor);
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp = v1.Patch.CompareTo(v2.Patch);
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComparePrerelease(v1.Prerelease, v2.Prerelease);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Sort order is consistent with precedence order, but provides an order for versions with the same precedence. Sort order is determined by comparing the major, minor, patch, prerelease portion, and build metadata in order from left to right. The major, minor, and patch version numbers are compared numerically. A prerelease version precedes a release version.</para>
|
||||||
|
/// <para>The prerelease portion is compared by comparing each prerelease identifier from left to right. Numeric prerelease identifiers precede alphanumeric identifiers. Numeric identifiers are compared numerically. Alphanumeric identifiers are compared lexically in ASCII sort order. A longer series of prerelease identifiers follows a shorter series if all the preceding identifiers are equal.</para>
|
||||||
|
/// <para>Otherwise, equal versions without build metadata precede those with metadata. The build metadata is compared by comparing each metadata identifier. Identifiers are compared lexically in ASCII sort order. A longer series of metadata identifiers follows a shorter series if all the preceding identifiers are equal.</para>
|
||||||
|
/// </remarks>
|
||||||
|
private sealed class SortOrderSemanticVersionComparer : SemanticVersionComparer
|
||||||
|
{
|
||||||
|
public override bool Equals(SemanticVersion? x, SemanticVersion? y)
|
||||||
|
{
|
||||||
|
if (x is null && y is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null || y is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v1 = x.Value;
|
||||||
|
var v2 = y.Value;
|
||||||
|
|
||||||
|
return v1.Major == v2.Major &&
|
||||||
|
v1.Minor == v2.Minor &&
|
||||||
|
v1.Patch == v2.Patch &&
|
||||||
|
string.Equals(v1.Prerelease, v2.Prerelease, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(v1.Metadata, v2.Metadata, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode(SemanticVersion? v)
|
||||||
|
{
|
||||||
|
if (v is null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ver = v.Value;
|
||||||
|
return HashCode.Combine(ver.Major, ver.Minor, ver.Patch, ver.Prerelease, ver.Metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Compare(SemanticVersion? x, SemanticVersion? y)
|
||||||
|
{
|
||||||
|
var cmp = Priority.Compare(x, y);
|
||||||
|
return cmp is not 0 ? cmp : CompareMetadata(x?.Metadata, y?.Metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ComparePrerelease(string? x, string? y)
|
||||||
|
{
|
||||||
|
if (string.Equals(x, y, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xParts = x.Split('.');
|
||||||
|
var yParts = y.Split('.');
|
||||||
|
|
||||||
|
var length = Math.Min(xParts.Length, yParts.Length);
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var xPart = xParts[i];
|
||||||
|
var yPart = yParts[i];
|
||||||
|
|
||||||
|
var xIsNumeric = ulong.TryParse(xPart, out var xNum);
|
||||||
|
var yIsNumeric = ulong.TryParse(yPart, out var yNum);
|
||||||
|
|
||||||
|
if (xIsNumeric && yIsNumeric)
|
||||||
|
{
|
||||||
|
var cmp = xNum.CompareTo(yNum);
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (xIsNumeric)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (yIsNumeric)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cmp = string.CompareOrdinal(xPart, yPart);
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xParts.Length.CompareTo(yParts.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompareMetadata(string? x, string? y)
|
||||||
|
{
|
||||||
|
if (string.Equals(x, y, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y is null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xParts = x.Split('.');
|
||||||
|
var yParts = y.Split('.');
|
||||||
|
|
||||||
|
var length = Math.Min(xParts.Length, yParts.Length);
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var cmp = string.CompareOrdinal(xParts[i], yParts[i]);
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xParts.Length.CompareTo(yParts.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/semver/SemanticVersionRange.Formatting.cs
Normal file
226
src/semver/SemanticVersionRange.Formatting.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return ToString(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IFormattable
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||||
|
{
|
||||||
|
if (format is not null and not "m" and not "n" and not "ns")
|
||||||
|
{
|
||||||
|
throw new FormatException($"The format string '{format}' is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||||
|
handler.AppendFormatted(this, format);
|
||||||
|
return handler.ToStringAndClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISpanFormattable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to format the semantic version range into the specified span of characters.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryFormat(Span{char},out int,ReadOnlySpan{char},IFormatProvider)">ISpanFormattable.TryFormat</see>
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten)
|
||||||
|
{
|
||||||
|
return TryFormat(destination, out charsWritten, default, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
|
||||||
|
if (format.IsEmpty)
|
||||||
|
{
|
||||||
|
format = "ns";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format is not "m" and not "n" and not "ns")
|
||||||
|
{
|
||||||
|
throw new FormatException($"The format string '{format}' is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = new SpanBuffer(destination);
|
||||||
|
var sets = _sets ?? [];
|
||||||
|
|
||||||
|
if (format is "m")
|
||||||
|
{
|
||||||
|
for (var i = 0; i < sets.Length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0 && !buf.TryWrite(','))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryFormatMaven(ref buf, sets[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < sets.Length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0 && !buf.TryWrite(" || "))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format is "ns")
|
||||||
|
{
|
||||||
|
if (!TryFormatSimpleNpm(ref buf, sets[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!TryFormatNormalNpm(ref buf, sets[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
charsWritten = buf.Written;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ConstraintSet set)
|
||||||
|
{
|
||||||
|
if (IsAny(set))
|
||||||
|
{
|
||||||
|
return buf.TryWrite('*');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
|
||||||
|
{
|
||||||
|
return buf.TryWrite(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set.Constraints.Length is not 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set is not { Lower: { Operation: Comparison.Gte or Comparison.Gt, Version: var lo } })
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set is not { Upper: { Operation: Comparison.Lt or Comparison.Lte, Version: var hi } upper })
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper.Operation is Comparison.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
||||||
|
{
|
||||||
|
if (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
|
||||||
|
{
|
||||||
|
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lo is { Major: 0, Minor: > 0 } && hi is { Major: 0 } && hi.Minor == lo.Minor + 1)
|
||||||
|
{
|
||||||
|
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lo is { Major: 0, Minor: 0 } && hi is { Major: 0, Minor: 0 } && hi.Patch == lo.Patch + 1)
|
||||||
|
{
|
||||||
|
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper.Operation is Comparison.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
|
||||||
|
{
|
||||||
|
if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1)
|
||||||
|
{
|
||||||
|
return buf.TryWrite('~') && buf.TryWrite(lo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFormatNormalNpm(ref SpanBuffer buf, ConstraintSet set)
|
||||||
|
{
|
||||||
|
if (IsAny(set))
|
||||||
|
{
|
||||||
|
return buf.TryWrite('*');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < set.Constraints.Length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0 && !buf.TryWrite(' '))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var @operator = set.Constraints[i].Operation switch
|
||||||
|
{
|
||||||
|
Comparison.Neq => "!=",
|
||||||
|
Comparison.Lt => "<",
|
||||||
|
Comparison.Lte => "<=",
|
||||||
|
Comparison.Gt => ">",
|
||||||
|
Comparison.Gte => ">=",
|
||||||
|
Comparison.Eq or _ => ReadOnlySpan<char>.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!@operator.IsEmpty && !buf.TryWrite(@operator))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buf.TryWrite(set.Constraints[i].Version))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFormatMaven(ref SpanBuffer buf, ConstraintSet set)
|
||||||
|
{
|
||||||
|
if (IsAny(set))
|
||||||
|
{
|
||||||
|
return buf.TryWrite("(,)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
|
||||||
|
{
|
||||||
|
return buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.TryWrite(set.Lower?.Operation == Comparison.Gte ? '[' : '(') &&
|
||||||
|
(!set.Lower.HasValue || buf.TryWrite(set.Lower.Value.Version)) &&
|
||||||
|
buf.TryWrite(',') &&
|
||||||
|
(!set.Upper.HasValue || buf.TryWrite(set.Upper.Value.Version)) &&
|
||||||
|
buf.TryWrite(set.Upper?.Operation == Comparison.Lte ? ']' : ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAny(ConstraintSet set)
|
||||||
|
{
|
||||||
|
return set.Constraints is [] or [{ Operation: Comparison.Gte, Version: { Major: 0, Minor: 0, Patch: 0 } }];
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/semver/SemanticVersionRange.JsonConverter.cs
Normal file
41
src/semver/SemanticVersionRange.JsonConverter.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||||
|
public readonly partial record struct SemanticVersionRange
|
||||||
|
{
|
||||||
|
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||||
|
{
|
||||||
|
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType is JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
|
||||||
|
{
|
||||||
|
throw new JsonException("Expected string");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Parse(value);
|
||||||
|
}
|
||||||
|
catch (FormatException exception)
|
||||||
|
{
|
||||||
|
throw new JsonException(exception.Message, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
515
src/semver/SemanticVersionRange.Parsing.cs
Normal file
515
src/semver/SemanticVersionRange.Parsing.cs
Normal file
|
|
@ -0,0 +1,515 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||||
|
{
|
||||||
|
#region IParsable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a string into a <see cref="SemanticVersionRange"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||||
|
public static SemanticVersionRange Parse(string s)
|
||||||
|
{
|
||||||
|
return Parse(s.AsSpan(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
return Parse(s.AsSpan(), provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a string into a <see cref="SemanticVersionRange"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersionRange)"/>
|
||||||
|
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||||
|
{
|
||||||
|
return TryParse(s, null, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||||
|
{
|
||||||
|
return TryParse(s.AsSpan(), provider, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISpanParsable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||||
|
public static SemanticVersionRange Parse(ReadOnlySpan<char> s)
|
||||||
|
{
|
||||||
|
return Parse(s, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static SemanticVersionRange Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
if (!TryParse(s, provider, out var result))
|
||||||
|
{
|
||||||
|
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersionRange)"/>
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||||
|
{
|
||||||
|
return TryParse(s, null, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
|
||||||
|
if (s.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[0] is '[' or '(' ? TryParseMaven(s, out result) : TryParseNpm(s, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
var sets = new List<ConstraintSet>();
|
||||||
|
|
||||||
|
foreach (var range in new NpmSetGrouping(s))
|
||||||
|
{
|
||||||
|
if (!TryParseNpmSet(s[range].Trim(), out var set))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new SemanticVersionRange([.. sets]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ref struct NpmSetGrouping
|
||||||
|
{
|
||||||
|
private readonly ReadOnlySpan<char> _span;
|
||||||
|
private int _currentStart;
|
||||||
|
private int _currentEnd;
|
||||||
|
|
||||||
|
public NpmSetGrouping(ReadOnlySpan<char> span)
|
||||||
|
{
|
||||||
|
_span = span;
|
||||||
|
_currentStart = 0;
|
||||||
|
_currentEnd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly Range Current => _currentStart.._currentEnd;
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 2;
|
||||||
|
|
||||||
|
if (_currentStart >= _span.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _span[_currentStart..].IndexOf("||");
|
||||||
|
_currentEnd = index >= 0 ? _currentStart + index : _span.Length;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly NpmSetGrouping GetEnumerator()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseNpmSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||||
|
{
|
||||||
|
set = default;
|
||||||
|
|
||||||
|
if (s.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hyphen range: "1.0.0 - 2.0.0" OR Caret: "^x.y.z" OR Tilde: "~x.y.z"
|
||||||
|
if (TryParseHyphenRange(s, out set) || TryParseCaretRange(s, out set) || TryParseTildeRange(s, out set))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparators = new List<Constraint>();
|
||||||
|
foreach (var range in s.Split(' '))
|
||||||
|
{
|
||||||
|
if (s[range] is not { IsEmpty: false } segment)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryParseCompare(segment, comparators))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparators.Count is 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set = new ConstraintSet([.. comparators]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||||
|
{
|
||||||
|
set = default;
|
||||||
|
|
||||||
|
for (var i = 0; i < s.Length; i++)
|
||||||
|
{
|
||||||
|
if (s[i..] is [' ', '-', ' ', ..])
|
||||||
|
{
|
||||||
|
if (!SemanticVersion.TryParse(s[..(i + 0)].Trim(), null, out var lo))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SemanticVersion.TryParse(s[(i + 3)..].Trim(), null, out var hi))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lte, hi)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||||
|
{
|
||||||
|
set = default;
|
||||||
|
|
||||||
|
if (s.IsEmpty || s[0] is not '^')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s[1..];
|
||||||
|
|
||||||
|
if (!SemanticVersion.TryParsePartially(s, out var lo, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemanticVersion hi;
|
||||||
|
|
||||||
|
if (lo.Major > 0)
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||||
|
}
|
||||||
|
else if (lo.Minor > 0)
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(0, lo.Minor + 1, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(0, 0, lo.Patch + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||||
|
{
|
||||||
|
set = default;
|
||||||
|
|
||||||
|
if (s.IsEmpty || s[0] is not '~')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s[1..];
|
||||||
|
|
||||||
|
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemanticVersion hi;
|
||||||
|
|
||||||
|
if (wildcard is 1)
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseCompare(ReadOnlySpan<char> s, List<Constraint> constraints)
|
||||||
|
{
|
||||||
|
var op = Comparison.Eq;
|
||||||
|
|
||||||
|
if (TryParseComparatorPrefix(s, out var comparison, out var rest))
|
||||||
|
{
|
||||||
|
op = comparison;
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SemanticVersion.TryParsePartially(s, out var lo, out var components))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (components is 0)
|
||||||
|
{
|
||||||
|
constraints.Add(new Constraint(Comparison.Gte, new SemanticVersion(0, 0, 0)));
|
||||||
|
return op is Comparison.Eq;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (components is 3)
|
||||||
|
{
|
||||||
|
constraints.Add(new Constraint(op, lo));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemanticVersion hi;
|
||||||
|
|
||||||
|
if (components is 1)
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case Comparison.Eq:
|
||||||
|
case Comparison.Gte:
|
||||||
|
constraints.Add(new Constraint(Comparison.Gte, lo));
|
||||||
|
constraints.Add(new Constraint(Comparison.Lt, hi));
|
||||||
|
return true;
|
||||||
|
case Comparison.Gt:
|
||||||
|
constraints.Add(new Constraint(Comparison.Gt, lo));
|
||||||
|
constraints.Add(new Constraint(Comparison.Lt, hi));
|
||||||
|
return true;
|
||||||
|
case Comparison.Lte:
|
||||||
|
constraints.Add(new Constraint(Comparison.Lt, hi));
|
||||||
|
return true;
|
||||||
|
case Comparison.Lt:
|
||||||
|
constraints.Add(new Constraint(Comparison.Lt, lo));
|
||||||
|
return true;
|
||||||
|
case Comparison.Neq:
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out Comparison op, out ReadOnlySpan<char> remainder)
|
||||||
|
{
|
||||||
|
if (s.IsEmpty)
|
||||||
|
{
|
||||||
|
op = default;
|
||||||
|
remainder = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case ['!', '=', ..]:
|
||||||
|
op = Comparison.Neq;
|
||||||
|
remainder = s[2..];
|
||||||
|
return true;
|
||||||
|
case ['>', '=', ..]:
|
||||||
|
op = Comparison.Gte;
|
||||||
|
remainder = s[2..];
|
||||||
|
return true;
|
||||||
|
case ['<', '=', ..]:
|
||||||
|
op = Comparison.Lte;
|
||||||
|
remainder = s[2..];
|
||||||
|
return true;
|
||||||
|
case ['>', ..]:
|
||||||
|
op = Comparison.Gt;
|
||||||
|
remainder = s[1..];
|
||||||
|
return true;
|
||||||
|
case ['<', ..]:
|
||||||
|
op = Comparison.Lt;
|
||||||
|
remainder = s[1..];
|
||||||
|
return true;
|
||||||
|
case ['=', ..]:
|
||||||
|
op = Comparison.Eq;
|
||||||
|
remainder = s[1..];
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
remainder = s;
|
||||||
|
op = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
var sets = new List<ConstraintSet>();
|
||||||
|
|
||||||
|
foreach (var range in new MavenSetGrouping(s))
|
||||||
|
{
|
||||||
|
if (!TryParseMavenSet(s[range].Trim(), out var set))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new SemanticVersionRange([.. sets]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ref struct MavenSetGrouping
|
||||||
|
{
|
||||||
|
private readonly ReadOnlySpan<char> _span;
|
||||||
|
private int _currentStart;
|
||||||
|
private int _currentEnd;
|
||||||
|
|
||||||
|
public MavenSetGrouping(ReadOnlySpan<char> readOnlySpan)
|
||||||
|
{
|
||||||
|
_span = readOnlySpan;
|
||||||
|
_currentStart = 0;
|
||||||
|
_currentEnd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly Range Current => _currentStart.._currentEnd;
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
|
||||||
|
|
||||||
|
if (_currentStart >= _span.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var depth = 0;
|
||||||
|
var i = _currentStart;
|
||||||
|
|
||||||
|
while (i < _span.Length)
|
||||||
|
{
|
||||||
|
var ch = _span[i];
|
||||||
|
|
||||||
|
if (ch is '[' or '(')
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
else if (ch is ']' or ')')
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
else if (ch is ',' && depth is 0)
|
||||||
|
{
|
||||||
|
_currentEnd = i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentEnd = _span.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly MavenSetGrouping GetEnumerator()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseMavenSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||||
|
{
|
||||||
|
set = default;
|
||||||
|
|
||||||
|
var loInclusive = s[0] is '[';
|
||||||
|
var hiInclusive = s[^1] is ']';
|
||||||
|
|
||||||
|
s = s[1..^1];
|
||||||
|
|
||||||
|
if (s.IndexOf(',') is not (>= 0 and var i))
|
||||||
|
{
|
||||||
|
if (!loInclusive || !hiInclusive)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set = new ConstraintSet([new Constraint(Comparison.Eq, version)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comps = new List<Constraint>();
|
||||||
|
|
||||||
|
if (s[..i].Trim() is { IsEmpty: false } loStr)
|
||||||
|
{
|
||||||
|
if (!SemanticVersion.TryParsePartially(loStr, out var lo, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
comps.Add(new Constraint(loInclusive ? Comparison.Gte : Comparison.Gt, lo));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s[(i + 1)..].Trim() is { IsEmpty: false } hiStr)
|
||||||
|
{
|
||||||
|
if (!SemanticVersion.TryParsePartially(hiStr, out var hi, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
comps.Add(new Constraint(hiInclusive ? Comparison.Lte : Comparison.Lt, hi));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for exact match [a,a]
|
||||||
|
if (comps is [{ Operation: Comparison.Gte, Version: var lhs }, { Operation: Comparison.Lte, Version: var rhs }])
|
||||||
|
{
|
||||||
|
if (lhs == rhs)
|
||||||
|
{
|
||||||
|
set = new ConstraintSet([new Constraint(Comparison.Eq, lhs)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set = new ConstraintSet([.. comps]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/semver/SemanticVersionRange.cs
Normal file
105
src/semver/SemanticVersionRange.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a semantic version range, which is a set of version constraints
|
||||||
|
/// used to match specific semantic versions based on defined ranges or patterns.
|
||||||
|
/// </summary>
|
||||||
|
public readonly partial record struct SemanticVersionRange
|
||||||
|
{
|
||||||
|
// OR of AND-groups. null == empty range (matches nothing).
|
||||||
|
private readonly ConstraintSet[]? _sets;
|
||||||
|
|
||||||
|
internal SemanticVersionRange(ConstraintSet[] sets)
|
||||||
|
{
|
||||||
|
_sets = sets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the specified version is contained in the range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="version">The version to check.</param>
|
||||||
|
/// <returns><c>true</c> if the version is contained in the range, otherwise <c>false</c>.</returns>
|
||||||
|
public bool Contains(SemanticVersion version)
|
||||||
|
{
|
||||||
|
if (_sets is null || _sets.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _sets.Any(set => set.Includes(version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||||
|
|
||||||
|
internal readonly struct Constraint
|
||||||
|
{
|
||||||
|
public readonly Comparison Operation;
|
||||||
|
public readonly SemanticVersion Version;
|
||||||
|
|
||||||
|
public Constraint(Comparison operation, SemanticVersion version)
|
||||||
|
{
|
||||||
|
Operation = operation;
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Includes(SemanticVersion v)
|
||||||
|
{
|
||||||
|
return Operation switch
|
||||||
|
{
|
||||||
|
Comparison.Eq => v == Version,
|
||||||
|
Comparison.Neq => v != Version,
|
||||||
|
Comparison.Lt => v < Version,
|
||||||
|
Comparison.Lte => v <= Version,
|
||||||
|
Comparison.Gt => v > Version,
|
||||||
|
Comparison.Gte => v >= Version,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Operation switch
|
||||||
|
{
|
||||||
|
Comparison.Neq => $"!={Version}",
|
||||||
|
Comparison.Lt => $"<{Version}",
|
||||||
|
Comparison.Lte => $"<={Version}",
|
||||||
|
Comparison.Gt => $">{Version}",
|
||||||
|
Comparison.Gte => $">={Version}",
|
||||||
|
Comparison.Eq or _ => $"{Version}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One AND-group of comparators (all must be satisfied).
|
||||||
|
internal readonly struct ConstraintSet
|
||||||
|
{
|
||||||
|
public readonly Constraint[] Constraints;
|
||||||
|
|
||||||
|
public ConstraintSet(Constraint[] constraints)
|
||||||
|
{
|
||||||
|
Constraints = constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Constraint? Upper => Constraints.Where(c => c.Operation is Comparison.Lt or Comparison.Lte)
|
||||||
|
.Select(it => (Constraint?)it).FirstOrDefault();
|
||||||
|
|
||||||
|
public Constraint? Lower => Constraints.Where(c => c.Operation is Comparison.Gt or Comparison.Gte)
|
||||||
|
.Select(it => (Constraint?)it).FirstOrDefault();
|
||||||
|
|
||||||
|
public bool Includes(SemanticVersion v)
|
||||||
|
{
|
||||||
|
foreach (var c in Constraints)
|
||||||
|
{
|
||||||
|
if (!c.Includes(v))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/semver/SpanBuffer.cs
Normal file
43
src/semver/SpanBuffer.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
internal ref struct SpanBuffer(Span<char> buffer)
|
||||||
|
{
|
||||||
|
private readonly Span<char> _buffer = buffer;
|
||||||
|
public int Written { get; private set; }
|
||||||
|
|
||||||
|
public bool TryWrite(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (!value.TryCopyTo(_buffer[Written..]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Written += value.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite<T>(T v) where T : ISpanFormattable
|
||||||
|
{
|
||||||
|
if (!v.TryFormat(_buffer[Written..], out var n, [], null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Written += n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite<T>(T v, ReadOnlySpan<char> fmt) where T : ISpanFormattable
|
||||||
|
{
|
||||||
|
if (!v.TryFormat(_buffer[Written..], out var n, fmt, null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Written += n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/semver/package-icon.png
Normal file
BIN
src/semver/package-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
70
src/semver/package-readme.md
Normal file
70
src/semver/package-readme.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
SemVer is a .NET library for parsing, comparing, formatting, and serializing semantic versions and version ranges.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Parsing:** parse semantic version strings into structured objects.
|
||||||
|
- **Comparison:** compare versions with operators or `CompareTo`.
|
||||||
|
- **Ranges:** evaluate npm-style and Maven-style version ranges.
|
||||||
|
- **Serialization:** use `System.Text.Json` converters for versions and ranges.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Install the NuGet package:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet add package Geekeey.SemVer
|
||||||
|
```
|
||||||
|
|
||||||
|
You may need to add our NuGet feed to your `nuget.config` this can be done by running the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet nuget add source -n geekeey https://code.geekeey.de/api/packages/geekeey/nuget/index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
var version = SemanticVersion.Parse("1.2.3-beta+build.7");
|
||||||
|
var next = new SemanticVersion(1, 3, 0);
|
||||||
|
|
||||||
|
Console.WriteLine(version);
|
||||||
|
Console.WriteLine(version.Major);
|
||||||
|
Console.WriteLine(version.Prerelease);
|
||||||
|
Console.WriteLine(version.Metadata);
|
||||||
|
Console.WriteLine(version < next);
|
||||||
|
|
||||||
|
if (SemanticVersion.TryParse("1.2.3-alpha", out var parsed))
|
||||||
|
{
|
||||||
|
Console.WriteLine(parsed);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Version ranges support npm-style and Maven-style syntax.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
var npmRange = SemanticVersionRange.Parse("^1.2.3");
|
||||||
|
var mavenRange = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
var candidate = SemanticVersion.Parse("1.4.2");
|
||||||
|
|
||||||
|
Console.WriteLine(npmRange.Contains(candidate));
|
||||||
|
Console.WriteLine(mavenRange.Contains(candidate));
|
||||||
|
Console.WriteLine(npmRange.ToString("m", null));
|
||||||
|
Console.WriteLine(mavenRange.ToString("ns", null));
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `SemanticVersion` and `SemanticVersionRange` integrate with `System.Text.Json`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Text.Json;
|
||||||
|
using Geekeey.SemVer;
|
||||||
|
|
||||||
|
var versionJson = JsonSerializer.Serialize(SemanticVersion.Parse("1.2.3-beta+build.7"));
|
||||||
|
var rangeJson = JsonSerializer.Serialize(SemanticVersionRange.Parse("^1.2.3"));
|
||||||
|
|
||||||
|
var roundTrippedVersion = JsonSerializer.Deserialize<SemanticVersion>(versionJson);
|
||||||
|
var roundTrippedRange = JsonSerializer.Deserialize<SemanticVersionRange>(rangeJson);
|
||||||
|
```
|
||||||
Loading…
Add table
Add a link
Reference in a new issue