diff --git a/.checkstyle/checkstyle-suppressions.xml b/.checkstyle/checkstyle-suppressions.xml
new file mode 100644
index 0000000..13997f0
--- /dev/null
+++ b/.checkstyle/checkstyle-suppressions.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/.checkstyle/checkstyle.xml b/.checkstyle/checkstyle.xml
new file mode 100644
index 0000000..dc19a49
--- /dev/null
+++ b/.checkstyle/checkstyle.xml
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..fd553dc
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,1014 @@
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 130
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = false
+ij_smart_tabs = false
+ij_wrap_on_typing = true
+
+[*.conf]
+indent_size = 2
+tab_width = 2
+ij_continuation_indent_size = 2
+ij_hocon_keep_blank_lines_before_right_brace = 2
+ij_hocon_keep_indents_on_empty_lines = false
+ij_hocon_keep_line_breaks = true
+ij_hocon_space_after_colon = true
+ij_hocon_space_after_comma = true
+ij_hocon_space_before_colon = true
+ij_hocon_space_before_comma = false
+ij_hocon_spaces_within_braces = false
+ij_hocon_spaces_within_brackets = false
+ij_hocon_spaces_within_method_call_parentheses = false
+
+[*.css]
+ij_css_align_closing_brace_with_properties = false
+ij_css_blank_lines_around_nested_selector = 1
+ij_css_blank_lines_between_blocks = 1
+ij_css_brace_placement = end_of_line
+ij_css_enforce_quotes_on_format = false
+ij_css_hex_color_long_format = false
+ij_css_hex_color_lower_case = false
+ij_css_hex_color_short_format = false
+ij_css_hex_color_upper_case = false
+ij_css_keep_blank_lines_in_code = 2
+ij_css_keep_indents_on_empty_lines = false
+ij_css_keep_single_line_blocks = false
+ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
+ij_css_space_after_colon = true
+ij_css_space_before_opening_brace = true
+ij_css_use_double_quotes = true
+ij_css_value_alignment = do_not_align
+
+[*.java]
+ij_java_align_consecutive_assignments = false
+ij_java_align_consecutive_variable_declarations = false
+ij_java_align_group_field_declarations = false
+ij_java_align_multiline_annotation_parameters = false
+ij_java_align_multiline_array_initializer_expression = false
+ij_java_align_multiline_assignment = false
+ij_java_align_multiline_binary_operation = false
+ij_java_align_multiline_chained_methods = false
+ij_java_align_multiline_extends_list = false
+ij_java_align_multiline_for = true
+ij_java_align_multiline_method_parentheses = false
+ij_java_align_multiline_parameters = true
+ij_java_align_multiline_parameters_in_calls = false
+ij_java_align_multiline_parenthesized_expression = false
+ij_java_align_multiline_records = true
+ij_java_align_multiline_resources = true
+ij_java_align_multiline_ternary_operation = false
+ij_java_align_multiline_text_blocks = false
+ij_java_align_multiline_throws_list = false
+ij_java_align_subsequent_simple_methods = false
+ij_java_align_throws_keyword = false
+ij_java_annotation_parameter_wrap = off
+ij_java_array_initializer_new_line_after_left_brace = false
+ij_java_array_initializer_right_brace_on_new_line = false
+ij_java_array_initializer_wrap = off
+ij_java_assert_statement_colon_on_next_line = false
+ij_java_assert_statement_wrap = off
+ij_java_assignment_wrap = off
+ij_java_binary_operation_sign_on_next_line = false
+ij_java_binary_operation_wrap = off
+ij_java_blank_lines_after_anonymous_class_header = 0
+ij_java_blank_lines_after_class_header = 1
+ij_java_blank_lines_after_imports = 1
+ij_java_blank_lines_after_package = 1
+ij_java_blank_lines_around_class = 1
+ij_java_blank_lines_around_field = 0
+ij_java_blank_lines_around_field_in_interface = 0
+ij_java_blank_lines_around_initializer = 1
+ij_java_blank_lines_around_method = 1
+ij_java_blank_lines_around_method_in_interface = 1
+ij_java_blank_lines_before_class_end = 0
+ij_java_blank_lines_before_imports = 1
+ij_java_blank_lines_before_method_body = 0
+ij_java_blank_lines_before_package = 0
+ij_java_block_brace_style = end_of_line
+ij_java_block_comment_at_first_column = true
+ij_java_call_parameters_new_line_after_left_paren = true
+ij_java_call_parameters_right_paren_on_new_line = true
+ij_java_call_parameters_wrap = on_every_item
+ij_java_case_statement_on_separate_line = true
+ij_java_catch_on_new_line = false
+ij_java_class_annotation_wrap = split_into_lines
+ij_java_class_brace_style = end_of_line
+ij_java_class_count_to_use_import_on_demand = 100000
+ij_java_class_names_in_javadoc = 1
+ij_java_do_not_indent_top_level_class_members = false
+ij_java_do_not_wrap_after_single_annotation = false
+ij_java_do_while_brace_force = always
+ij_java_doc_add_blank_line_after_description = true
+ij_java_doc_add_blank_line_after_param_comments = false
+ij_java_doc_add_blank_line_after_return = false
+ij_java_doc_add_p_tag_on_empty_lines = true
+ij_java_doc_align_exception_comments = true
+ij_java_doc_align_param_comments = true
+ij_java_doc_do_not_wrap_if_one_line = false
+ij_java_doc_enable_formatting = true
+ij_java_doc_enable_leading_asterisks = true
+ij_java_doc_indent_on_continuation = true
+ij_java_doc_keep_empty_lines = true
+ij_java_doc_keep_empty_parameter_tag = true
+ij_java_doc_keep_empty_return_tag = true
+ij_java_doc_keep_empty_throws_tag = true
+ij_java_doc_keep_invalid_tags = false
+ij_java_doc_param_description_on_new_line = false
+ij_java_doc_preserve_line_breaks = false
+ij_java_doc_use_throws_not_exception_tag = true
+ij_java_else_on_new_line = false
+ij_java_enum_constants_wrap = split_into_lines
+ij_java_extends_keyword_wrap = off
+ij_java_extends_list_wrap = normal
+ij_java_field_annotation_wrap = split_into_lines
+ij_java_finally_on_new_line = false
+ij_java_for_brace_force = always
+ij_java_for_statement_new_line_after_left_paren = false
+ij_java_for_statement_right_paren_on_new_line = false
+ij_java_for_statement_wrap = off
+ij_java_generate_final_locals = true
+ij_java_generate_final_parameters = true
+ij_java_if_brace_force = always
+ij_java_imports_layout = *,|,$*
+ij_java_indent_case_from_switch = true
+ij_java_insert_inner_class_imports = false
+ij_java_insert_override_annotation = true
+ij_java_keep_blank_lines_before_right_brace = 0
+ij_java_keep_blank_lines_between_package_declaration_and_header = 2
+ij_java_keep_blank_lines_in_code = 2
+ij_java_keep_blank_lines_in_declarations = 2
+ij_java_keep_control_statement_in_one_line = true
+ij_java_keep_first_column_comment = true
+ij_java_keep_indents_on_empty_lines = false
+ij_java_keep_line_breaks = true
+ij_java_keep_multiple_expressions_in_one_line = false
+ij_java_keep_simple_blocks_in_one_line = false
+ij_java_keep_simple_classes_in_one_line = false
+ij_java_keep_simple_lambdas_in_one_line = false
+ij_java_keep_simple_methods_in_one_line = false
+ij_java_label_indent_absolute = false
+ij_java_label_indent_size = 0
+ij_java_lambda_brace_style = end_of_line
+ij_java_layout_static_imports_separately = true
+ij_java_line_comment_add_space = false
+ij_java_line_comment_at_first_column = true
+ij_java_method_annotation_wrap = split_into_lines
+ij_java_method_brace_style = end_of_line
+ij_java_method_call_chain_wrap = on_every_item
+ij_java_method_parameters_new_line_after_left_paren = true
+ij_java_method_parameters_right_paren_on_new_line = true
+ij_java_method_parameters_wrap = on_every_item
+ij_java_modifier_list_wrap = false
+ij_java_names_count_to_use_import_on_demand = 100000
+ij_java_new_line_after_lparen_in_record_header = false
+ij_java_parameter_annotation_wrap = off
+ij_java_parentheses_expression_new_line_after_left_paren = false
+ij_java_parentheses_expression_right_paren_on_new_line = false
+ij_java_place_assignment_sign_on_next_line = false
+ij_java_prefer_longer_names = false
+ij_java_prefer_parameters_wrap = true
+ij_java_record_components_wrap = normal
+ij_java_repeat_synchronized = true
+ij_java_replace_instanceof_and_cast = false
+ij_java_replace_null_check = true
+ij_java_replace_sum_lambda_with_method_ref = true
+ij_java_resource_list_new_line_after_left_paren = false
+ij_java_resource_list_right_paren_on_new_line = false
+ij_java_resource_list_wrap = off
+ij_java_rparen_on_new_line_in_record_header = false
+ij_java_space_after_closing_angle_bracket_in_type_argument = false
+ij_java_space_after_colon = true
+ij_java_space_after_comma = true
+ij_java_space_after_comma_in_type_arguments = true
+ij_java_space_after_for_semicolon = true
+ij_java_space_after_quest = true
+ij_java_space_after_type_cast = true
+ij_java_space_before_annotation_array_initializer_left_brace = false
+ij_java_space_before_annotation_parameter_list = false
+ij_java_space_before_array_initializer_left_brace = false
+ij_java_space_before_catch_keyword = true
+ij_java_space_before_catch_left_brace = true
+ij_java_space_before_catch_parentheses = true
+ij_java_space_before_class_left_brace = true
+ij_java_space_before_colon = true
+ij_java_space_before_colon_in_foreach = true
+ij_java_space_before_comma = false
+ij_java_space_before_do_left_brace = true
+ij_java_space_before_else_keyword = true
+ij_java_space_before_else_left_brace = true
+ij_java_space_before_finally_keyword = true
+ij_java_space_before_finally_left_brace = true
+ij_java_space_before_for_left_brace = true
+ij_java_space_before_for_parentheses = true
+ij_java_space_before_for_semicolon = false
+ij_java_space_before_if_left_brace = true
+ij_java_space_before_if_parentheses = true
+ij_java_space_before_method_call_parentheses = false
+ij_java_space_before_method_left_brace = true
+ij_java_space_before_method_parentheses = false
+ij_java_space_before_opening_angle_bracket_in_type_parameter = false
+ij_java_space_before_quest = true
+ij_java_space_before_switch_left_brace = true
+ij_java_space_before_switch_parentheses = true
+ij_java_space_before_synchronized_left_brace = true
+ij_java_space_before_synchronized_parentheses = true
+ij_java_space_before_try_left_brace = true
+ij_java_space_before_try_parentheses = true
+ij_java_space_before_type_parameter_list = false
+ij_java_space_before_while_keyword = true
+ij_java_space_before_while_left_brace = true
+ij_java_space_before_while_parentheses = true
+ij_java_space_inside_one_line_enum_braces = false
+ij_java_space_within_empty_array_initializer_braces = false
+ij_java_space_within_empty_method_call_parentheses = false
+ij_java_space_within_empty_method_parentheses = false
+ij_java_spaces_around_additive_operators = true
+ij_java_spaces_around_assignment_operators = true
+ij_java_spaces_around_bitwise_operators = true
+ij_java_spaces_around_equality_operators = true
+ij_java_spaces_around_lambda_arrow = true
+ij_java_spaces_around_logical_operators = true
+ij_java_spaces_around_method_ref_dbl_colon = false
+ij_java_spaces_around_multiplicative_operators = true
+ij_java_spaces_around_relational_operators = true
+ij_java_spaces_around_shift_operators = true
+ij_java_spaces_around_type_bounds_in_type_parameters = true
+ij_java_spaces_around_unary_operator = false
+ij_java_spaces_within_angle_brackets = false
+ij_java_spaces_within_annotation_parentheses = false
+ij_java_spaces_within_array_initializer_braces = false
+ij_java_spaces_within_braces = false
+ij_java_spaces_within_brackets = false
+ij_java_spaces_within_cast_parentheses = false
+ij_java_spaces_within_catch_parentheses = false
+ij_java_spaces_within_for_parentheses = false
+ij_java_spaces_within_if_parentheses = false
+ij_java_spaces_within_method_call_parentheses = false
+ij_java_spaces_within_method_parentheses = false
+ij_java_spaces_within_parentheses = false
+ij_java_spaces_within_switch_parentheses = false
+ij_java_spaces_within_synchronized_parentheses = false
+ij_java_spaces_within_try_parentheses = false
+ij_java_spaces_within_while_parentheses = false
+ij_java_special_else_if_treatment = true
+ij_java_subclass_name_suffix = Impl
+ij_java_ternary_operation_signs_on_next_line = true
+ij_java_ternary_operation_wrap = on_every_item
+ij_java_test_name_suffix = Test
+ij_java_throws_keyword_wrap = off
+ij_java_throws_list_wrap = normal
+ij_java_use_external_annotations = false
+ij_java_use_fq_class_names = false
+ij_java_use_relative_indents = false
+ij_java_use_single_class_imports = true
+ij_java_variable_annotation_wrap = off
+ij_java_visibility = public
+ij_java_while_brace_force = always
+ij_java_while_on_new_line = false
+ij_java_wrap_comments = false
+ij_java_wrap_first_method_in_call_chain = true
+ij_java_wrap_long_lines = false
+
+[*.nbtt]
+max_line_length = 150
+ij_continuation_indent_size = 4
+ij_nbtt_keep_indents_on_empty_lines = false
+ij_nbtt_space_after_colon = true
+ij_nbtt_space_after_comma = true
+ij_nbtt_space_before_colon = true
+ij_nbtt_space_before_comma = false
+ij_nbtt_spaces_within_brackets = false
+ij_nbtt_spaces_within_parentheses = false
+
+[*.properties]
+ij_properties_align_group_field_declarations = false
+ij_properties_keep_blank_lines = false
+ij_properties_key_value_delimiter = equals
+ij_properties_spaces_around_key_value_delimiter = false
+
+[*.sass]
+indent_size = 2
+ij_sass_align_closing_brace_with_properties = false
+ij_sass_blank_lines_around_nested_selector = 1
+ij_sass_blank_lines_between_blocks = 1
+ij_sass_brace_placement = 0
+ij_sass_enforce_quotes_on_format = false
+ij_sass_hex_color_long_format = false
+ij_sass_hex_color_lower_case = false
+ij_sass_hex_color_short_format = false
+ij_sass_hex_color_upper_case = false
+ij_sass_keep_blank_lines_in_code = 2
+ij_sass_keep_indents_on_empty_lines = false
+ij_sass_keep_single_line_blocks = false
+ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
+ij_sass_space_after_colon = true
+ij_sass_space_before_opening_brace = true
+ij_sass_use_double_quotes = true
+ij_sass_value_alignment = 0
+
+[*.scss]
+indent_size = 2
+ij_scss_align_closing_brace_with_properties = false
+ij_scss_blank_lines_around_nested_selector = 1
+ij_scss_blank_lines_between_blocks = 1
+ij_scss_brace_placement = 0
+ij_scss_enforce_quotes_on_format = false
+ij_scss_hex_color_long_format = false
+ij_scss_hex_color_lower_case = false
+ij_scss_hex_color_short_format = false
+ij_scss_hex_color_upper_case = false
+ij_scss_keep_blank_lines_in_code = 2
+ij_scss_keep_indents_on_empty_lines = false
+ij_scss_keep_single_line_blocks = false
+ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
+ij_scss_space_after_colon = true
+ij_scss_space_before_opening_brace = true
+ij_scss_use_double_quotes = true
+ij_scss_value_alignment = 0
+
+[.editorconfig]
+ij_editorconfig_align_group_field_declarations = false
+ij_editorconfig_space_after_colon = false
+ij_editorconfig_space_after_comma = true
+ij_editorconfig_space_before_colon = false
+ij_editorconfig_space_before_comma = false
+ij_editorconfig_spaces_around_assignment_operators = true
+
+[{*.ant, *.fxml, *.jhm, *.jnlp, *.jrxml, *.pom, *.rng, *.tld, *.wsdl, *.xml, *.xsd, *.xsl, *.xslt, *.xul}]
+ij_xml_align_attributes = true
+ij_xml_align_text = false
+ij_xml_attribute_wrap = normal
+ij_xml_block_comment_at_first_column = true
+ij_xml_keep_blank_lines = 2
+ij_xml_keep_indents_on_empty_lines = false
+ij_xml_keep_line_breaks = true
+ij_xml_keep_line_breaks_in_text = true
+ij_xml_keep_whitespaces = false
+ij_xml_keep_whitespaces_around_cdata = preserve
+ij_xml_keep_whitespaces_inside_cdata = false
+ij_xml_line_comment_at_first_column = true
+ij_xml_space_after_tag_name = false
+ij_xml_space_around_equals_in_attribute = false
+ij_xml_space_inside_empty_tag = false
+ij_xml_text_wrap = normal
+
+[{*.ats, *.ts}]
+ij_continuation_indent_size = 4
+ij_typescript_align_imports = false
+ij_typescript_align_multiline_array_initializer_expression = false
+ij_typescript_align_multiline_binary_operation = false
+ij_typescript_align_multiline_chained_methods = false
+ij_typescript_align_multiline_extends_list = false
+ij_typescript_align_multiline_for = true
+ij_typescript_align_multiline_parameters = true
+ij_typescript_align_multiline_parameters_in_calls = false
+ij_typescript_align_multiline_ternary_operation = false
+ij_typescript_align_object_properties = 0
+ij_typescript_align_union_types = false
+ij_typescript_align_var_statements = 0
+ij_typescript_array_initializer_new_line_after_left_brace = false
+ij_typescript_array_initializer_right_brace_on_new_line = false
+ij_typescript_array_initializer_wrap = off
+ij_typescript_assignment_wrap = off
+ij_typescript_binary_operation_sign_on_next_line = false
+ij_typescript_binary_operation_wrap = off
+ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
+ij_typescript_blank_lines_after_imports = 1
+ij_typescript_blank_lines_around_class = 1
+ij_typescript_blank_lines_around_field = 0
+ij_typescript_blank_lines_around_field_in_interface = 0
+ij_typescript_blank_lines_around_function = 1
+ij_typescript_blank_lines_around_method = 1
+ij_typescript_blank_lines_around_method_in_interface = 1
+ij_typescript_block_brace_style = end_of_line
+ij_typescript_call_parameters_new_line_after_left_paren = false
+ij_typescript_call_parameters_right_paren_on_new_line = false
+ij_typescript_call_parameters_wrap = off
+ij_typescript_catch_on_new_line = false
+ij_typescript_chained_call_dot_on_new_line = true
+ij_typescript_class_brace_style = end_of_line
+ij_typescript_comma_on_new_line = false
+ij_typescript_do_while_brace_force = never
+ij_typescript_else_on_new_line = false
+ij_typescript_enforce_trailing_comma = keep
+ij_typescript_extends_keyword_wrap = off
+ij_typescript_extends_list_wrap = off
+ij_typescript_field_prefix = _
+ij_typescript_file_name_style = relaxed
+ij_typescript_finally_on_new_line = false
+ij_typescript_for_brace_force = never
+ij_typescript_for_statement_new_line_after_left_paren = false
+ij_typescript_for_statement_right_paren_on_new_line = false
+ij_typescript_for_statement_wrap = off
+ij_typescript_force_quote_style = false
+ij_typescript_force_semicolon_style = false
+ij_typescript_function_expression_brace_style = end_of_line
+ij_typescript_if_brace_force = never
+ij_typescript_import_merge_members = global
+ij_typescript_import_prefer_absolute_path = global
+ij_typescript_import_sort_members = true
+ij_typescript_import_sort_module_name = false
+ij_typescript_import_use_node_resolution = true
+ij_typescript_imports_wrap = on_every_item
+ij_typescript_indent_case_from_switch = true
+ij_typescript_indent_chained_calls = true
+ij_typescript_indent_package_children = 0
+ij_typescript_jsdoc_include_types = false
+ij_typescript_jsx_attribute_value = braces
+ij_typescript_keep_blank_lines_in_code = 2
+ij_typescript_keep_first_column_comment = true
+ij_typescript_keep_indents_on_empty_lines = false
+ij_typescript_keep_line_breaks = true
+ij_typescript_keep_simple_blocks_in_one_line = false
+ij_typescript_keep_simple_methods_in_one_line = false
+ij_typescript_line_comment_add_space = true
+ij_typescript_line_comment_at_first_column = false
+ij_typescript_method_brace_style = end_of_line
+ij_typescript_method_call_chain_wrap = off
+ij_typescript_method_parameters_new_line_after_left_paren = false
+ij_typescript_method_parameters_right_paren_on_new_line = false
+ij_typescript_method_parameters_wrap = off
+ij_typescript_object_literal_wrap = on_every_item
+ij_typescript_parentheses_expression_new_line_after_left_paren = false
+ij_typescript_parentheses_expression_right_paren_on_new_line = false
+ij_typescript_place_assignment_sign_on_next_line = false
+ij_typescript_prefer_as_type_cast = false
+ij_typescript_prefer_explicit_types_function_expression_returns = false
+ij_typescript_prefer_explicit_types_function_returns = false
+ij_typescript_prefer_explicit_types_vars_fields = false
+ij_typescript_prefer_parameters_wrap = false
+ij_typescript_reformat_c_style_comments = false
+ij_typescript_space_after_colon = true
+ij_typescript_space_after_comma = true
+ij_typescript_space_after_dots_in_rest_parameter = false
+ij_typescript_space_after_generator_mult = true
+ij_typescript_space_after_property_colon = true
+ij_typescript_space_after_quest = true
+ij_typescript_space_after_type_colon = true
+ij_typescript_space_after_unary_not = false
+ij_typescript_space_before_async_arrow_lparen = true
+ij_typescript_space_before_catch_keyword = true
+ij_typescript_space_before_catch_left_brace = true
+ij_typescript_space_before_catch_parentheses = true
+ij_typescript_space_before_class_lbrace = true
+ij_typescript_space_before_class_left_brace = true
+ij_typescript_space_before_colon = true
+ij_typescript_space_before_comma = false
+ij_typescript_space_before_do_left_brace = true
+ij_typescript_space_before_else_keyword = true
+ij_typescript_space_before_else_left_brace = true
+ij_typescript_space_before_finally_keyword = true
+ij_typescript_space_before_finally_left_brace = true
+ij_typescript_space_before_for_left_brace = true
+ij_typescript_space_before_for_parentheses = true
+ij_typescript_space_before_for_semicolon = false
+ij_typescript_space_before_function_left_parenth = true
+ij_typescript_space_before_generator_mult = false
+ij_typescript_space_before_if_left_brace = true
+ij_typescript_space_before_if_parentheses = true
+ij_typescript_space_before_method_call_parentheses = false
+ij_typescript_space_before_method_left_brace = true
+ij_typescript_space_before_method_parentheses = false
+ij_typescript_space_before_property_colon = false
+ij_typescript_space_before_quest = true
+ij_typescript_space_before_switch_left_brace = true
+ij_typescript_space_before_switch_parentheses = true
+ij_typescript_space_before_try_left_brace = true
+ij_typescript_space_before_type_colon = false
+ij_typescript_space_before_unary_not = false
+ij_typescript_space_before_while_keyword = true
+ij_typescript_space_before_while_left_brace = true
+ij_typescript_space_before_while_parentheses = true
+ij_typescript_spaces_around_additive_operators = true
+ij_typescript_spaces_around_arrow_function_operator = true
+ij_typescript_spaces_around_assignment_operators = true
+ij_typescript_spaces_around_bitwise_operators = true
+ij_typescript_spaces_around_equality_operators = true
+ij_typescript_spaces_around_logical_operators = true
+ij_typescript_spaces_around_multiplicative_operators = true
+ij_typescript_spaces_around_relational_operators = true
+ij_typescript_spaces_around_shift_operators = true
+ij_typescript_spaces_around_unary_operator = false
+ij_typescript_spaces_within_array_initializer_brackets = false
+ij_typescript_spaces_within_brackets = false
+ij_typescript_spaces_within_catch_parentheses = false
+ij_typescript_spaces_within_for_parentheses = false
+ij_typescript_spaces_within_if_parentheses = false
+ij_typescript_spaces_within_imports = false
+ij_typescript_spaces_within_interpolation_expressions = false
+ij_typescript_spaces_within_method_call_parentheses = false
+ij_typescript_spaces_within_method_parentheses = false
+ij_typescript_spaces_within_object_literal_braces = false
+ij_typescript_spaces_within_object_type_braces = true
+ij_typescript_spaces_within_parentheses = false
+ij_typescript_spaces_within_switch_parentheses = false
+ij_typescript_spaces_within_type_assertion = false
+ij_typescript_spaces_within_union_types = true
+ij_typescript_spaces_within_while_parentheses = false
+ij_typescript_special_else_if_treatment = true
+ij_typescript_ternary_operation_signs_on_next_line = false
+ij_typescript_ternary_operation_wrap = off
+ij_typescript_union_types_wrap = on_every_item
+ij_typescript_use_chained_calls_group_indents = false
+ij_typescript_use_double_quotes = true
+ij_typescript_use_explicit_js_extension = global
+ij_typescript_use_path_mapping = always
+ij_typescript_use_public_modifier = false
+ij_typescript_use_semicolon_after_statement = true
+ij_typescript_var_declaration_wrap = normal
+ij_typescript_while_brace_force = never
+ij_typescript_while_on_new_line = false
+ij_typescript_wrap_comments = false
+
+[{*.bash, *.sh, *.zsh}]
+indent_size = 2
+tab_width = 2
+ij_shell_binary_ops_start_line = false
+ij_shell_keep_column_alignment_padding = false
+ij_shell_minify_program = false
+ij_shell_redirect_followed_by_space = false
+ij_shell_switch_cases_indented = false
+
+[{*.cjs, *.js}]
+ij_continuation_indent_size = 4
+ij_javascript_align_imports = false
+ij_javascript_align_multiline_array_initializer_expression = false
+ij_javascript_align_multiline_binary_operation = false
+ij_javascript_align_multiline_chained_methods = false
+ij_javascript_align_multiline_extends_list = false
+ij_javascript_align_multiline_for = true
+ij_javascript_align_multiline_parameters = true
+ij_javascript_align_multiline_parameters_in_calls = false
+ij_javascript_align_multiline_ternary_operation = false
+ij_javascript_align_object_properties = 0
+ij_javascript_align_union_types = false
+ij_javascript_align_var_statements = 0
+ij_javascript_array_initializer_new_line_after_left_brace = false
+ij_javascript_array_initializer_right_brace_on_new_line = false
+ij_javascript_array_initializer_wrap = off
+ij_javascript_assignment_wrap = off
+ij_javascript_binary_operation_sign_on_next_line = false
+ij_javascript_binary_operation_wrap = off
+ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
+ij_javascript_blank_lines_after_imports = 1
+ij_javascript_blank_lines_around_class = 1
+ij_javascript_blank_lines_around_field = 0
+ij_javascript_blank_lines_around_function = 1
+ij_javascript_blank_lines_around_method = 1
+ij_javascript_block_brace_style = end_of_line
+ij_javascript_call_parameters_new_line_after_left_paren = false
+ij_javascript_call_parameters_right_paren_on_new_line = false
+ij_javascript_call_parameters_wrap = off
+ij_javascript_catch_on_new_line = false
+ij_javascript_chained_call_dot_on_new_line = true
+ij_javascript_class_brace_style = end_of_line
+ij_javascript_comma_on_new_line = false
+ij_javascript_do_while_brace_force = never
+ij_javascript_else_on_new_line = false
+ij_javascript_enforce_trailing_comma = keep
+ij_javascript_extends_keyword_wrap = off
+ij_javascript_extends_list_wrap = off
+ij_javascript_field_prefix = _
+ij_javascript_file_name_style = relaxed
+ij_javascript_finally_on_new_line = false
+ij_javascript_for_brace_force = never
+ij_javascript_for_statement_new_line_after_left_paren = false
+ij_javascript_for_statement_right_paren_on_new_line = false
+ij_javascript_for_statement_wrap = off
+ij_javascript_force_quote_style = false
+ij_javascript_force_semicolon_style = false
+ij_javascript_function_expression_brace_style = end_of_line
+ij_javascript_if_brace_force = never
+ij_javascript_import_merge_members = global
+ij_javascript_import_prefer_absolute_path = global
+ij_javascript_import_sort_members = true
+ij_javascript_import_sort_module_name = false
+ij_javascript_import_use_node_resolution = true
+ij_javascript_imports_wrap = on_every_item
+ij_javascript_indent_case_from_switch = true
+ij_javascript_indent_chained_calls = true
+ij_javascript_indent_package_children = 0
+ij_javascript_jsx_attribute_value = braces
+ij_javascript_keep_blank_lines_in_code = 2
+ij_javascript_keep_first_column_comment = true
+ij_javascript_keep_indents_on_empty_lines = false
+ij_javascript_keep_line_breaks = true
+ij_javascript_keep_simple_blocks_in_one_line = false
+ij_javascript_keep_simple_methods_in_one_line = false
+ij_javascript_line_comment_add_space = true
+ij_javascript_line_comment_at_first_column = false
+ij_javascript_method_brace_style = end_of_line
+ij_javascript_method_call_chain_wrap = off
+ij_javascript_method_parameters_new_line_after_left_paren = false
+ij_javascript_method_parameters_right_paren_on_new_line = false
+ij_javascript_method_parameters_wrap = off
+ij_javascript_object_literal_wrap = on_every_item
+ij_javascript_parentheses_expression_new_line_after_left_paren = false
+ij_javascript_parentheses_expression_right_paren_on_new_line = false
+ij_javascript_place_assignment_sign_on_next_line = false
+ij_javascript_prefer_as_type_cast = false
+ij_javascript_prefer_explicit_types_function_expression_returns = false
+ij_javascript_prefer_explicit_types_function_returns = false
+ij_javascript_prefer_explicit_types_vars_fields = false
+ij_javascript_prefer_parameters_wrap = false
+ij_javascript_reformat_c_style_comments = false
+ij_javascript_space_after_colon = true
+ij_javascript_space_after_comma = true
+ij_javascript_space_after_dots_in_rest_parameter = false
+ij_javascript_space_after_generator_mult = true
+ij_javascript_space_after_property_colon = true
+ij_javascript_space_after_quest = true
+ij_javascript_space_after_type_colon = true
+ij_javascript_space_after_unary_not = false
+ij_javascript_space_before_async_arrow_lparen = true
+ij_javascript_space_before_catch_keyword = true
+ij_javascript_space_before_catch_left_brace = true
+ij_javascript_space_before_catch_parentheses = true
+ij_javascript_space_before_class_lbrace = true
+ij_javascript_space_before_class_left_brace = true
+ij_javascript_space_before_colon = true
+ij_javascript_space_before_comma = false
+ij_javascript_space_before_do_left_brace = true
+ij_javascript_space_before_else_keyword = true
+ij_javascript_space_before_else_left_brace = true
+ij_javascript_space_before_finally_keyword = true
+ij_javascript_space_before_finally_left_brace = true
+ij_javascript_space_before_for_left_brace = true
+ij_javascript_space_before_for_parentheses = true
+ij_javascript_space_before_for_semicolon = false
+ij_javascript_space_before_function_left_parenth = true
+ij_javascript_space_before_generator_mult = false
+ij_javascript_space_before_if_left_brace = true
+ij_javascript_space_before_if_parentheses = true
+ij_javascript_space_before_method_call_parentheses = false
+ij_javascript_space_before_method_left_brace = true
+ij_javascript_space_before_method_parentheses = false
+ij_javascript_space_before_property_colon = false
+ij_javascript_space_before_quest = true
+ij_javascript_space_before_switch_left_brace = true
+ij_javascript_space_before_switch_parentheses = true
+ij_javascript_space_before_try_left_brace = true
+ij_javascript_space_before_type_colon = false
+ij_javascript_space_before_unary_not = false
+ij_javascript_space_before_while_keyword = true
+ij_javascript_space_before_while_left_brace = true
+ij_javascript_space_before_while_parentheses = true
+ij_javascript_spaces_around_additive_operators = true
+ij_javascript_spaces_around_arrow_function_operator = true
+ij_javascript_spaces_around_assignment_operators = true
+ij_javascript_spaces_around_bitwise_operators = true
+ij_javascript_spaces_around_equality_operators = true
+ij_javascript_spaces_around_logical_operators = true
+ij_javascript_spaces_around_multiplicative_operators = true
+ij_javascript_spaces_around_relational_operators = true
+ij_javascript_spaces_around_shift_operators = true
+ij_javascript_spaces_around_unary_operator = false
+ij_javascript_spaces_within_array_initializer_brackets = false
+ij_javascript_spaces_within_brackets = false
+ij_javascript_spaces_within_catch_parentheses = false
+ij_javascript_spaces_within_for_parentheses = false
+ij_javascript_spaces_within_if_parentheses = false
+ij_javascript_spaces_within_imports = false
+ij_javascript_spaces_within_interpolation_expressions = false
+ij_javascript_spaces_within_method_call_parentheses = false
+ij_javascript_spaces_within_method_parentheses = false
+ij_javascript_spaces_within_object_literal_braces = false
+ij_javascript_spaces_within_object_type_braces = true
+ij_javascript_spaces_within_parentheses = false
+ij_javascript_spaces_within_switch_parentheses = false
+ij_javascript_spaces_within_type_assertion = false
+ij_javascript_spaces_within_union_types = true
+ij_javascript_spaces_within_while_parentheses = false
+ij_javascript_special_else_if_treatment = true
+ij_javascript_ternary_operation_signs_on_next_line = false
+ij_javascript_ternary_operation_wrap = off
+ij_javascript_union_types_wrap = on_every_item
+ij_javascript_use_chained_calls_group_indents = false
+ij_javascript_use_double_quotes = true
+ij_javascript_use_explicit_js_extension = global
+ij_javascript_use_path_mapping = always
+ij_javascript_use_public_modifier = false
+ij_javascript_use_semicolon_after_statement = true
+ij_javascript_var_declaration_wrap = normal
+ij_javascript_while_brace_force = never
+ij_javascript_while_on_new_line = false
+ij_javascript_wrap_comments = false
+
+[{*.ft, *.vm, *.vsl}]
+ij_vtl_keep_indents_on_empty_lines = false
+
+[{*.gant, *.gradle, *.groovy, *.gy}]
+ij_groovy_align_group_field_declarations = false
+ij_groovy_align_multiline_array_initializer_expression = false
+ij_groovy_align_multiline_assignment = false
+ij_groovy_align_multiline_binary_operation = false
+ij_groovy_align_multiline_chained_methods = false
+ij_groovy_align_multiline_extends_list = false
+ij_groovy_align_multiline_for = true
+ij_groovy_align_multiline_list_or_map = true
+ij_groovy_align_multiline_method_parentheses = false
+ij_groovy_align_multiline_parameters = true
+ij_groovy_align_multiline_parameters_in_calls = false
+ij_groovy_align_multiline_resources = true
+ij_groovy_align_multiline_ternary_operation = false
+ij_groovy_align_multiline_throws_list = false
+ij_groovy_align_named_args_in_map = true
+ij_groovy_align_throws_keyword = false
+ij_groovy_array_initializer_new_line_after_left_brace = false
+ij_groovy_array_initializer_right_brace_on_new_line = false
+ij_groovy_array_initializer_wrap = off
+ij_groovy_assert_statement_wrap = off
+ij_groovy_assignment_wrap = off
+ij_groovy_binary_operation_wrap = off
+ij_groovy_blank_lines_after_class_header = 0
+ij_groovy_blank_lines_after_imports = 1
+ij_groovy_blank_lines_after_package = 1
+ij_groovy_blank_lines_around_class = 1
+ij_groovy_blank_lines_around_field = 0
+ij_groovy_blank_lines_around_field_in_interface = 0
+ij_groovy_blank_lines_around_method = 1
+ij_groovy_blank_lines_around_method_in_interface = 1
+ij_groovy_blank_lines_before_imports = 1
+ij_groovy_blank_lines_before_method_body = 0
+ij_groovy_blank_lines_before_package = 0
+ij_groovy_block_brace_style = end_of_line
+ij_groovy_block_comment_at_first_column = true
+ij_groovy_call_parameters_new_line_after_left_paren = false
+ij_groovy_call_parameters_right_paren_on_new_line = false
+ij_groovy_call_parameters_wrap = off
+ij_groovy_catch_on_new_line = false
+ij_groovy_class_annotation_wrap = split_into_lines
+ij_groovy_class_brace_style = end_of_line
+ij_groovy_class_count_to_use_import_on_demand = 5
+ij_groovy_do_while_brace_force = never
+ij_groovy_else_on_new_line = false
+ij_groovy_enum_constants_wrap = off
+ij_groovy_extends_keyword_wrap = off
+ij_groovy_extends_list_wrap = off
+ij_groovy_field_annotation_wrap = split_into_lines
+ij_groovy_finally_on_new_line = false
+ij_groovy_for_brace_force = never
+ij_groovy_for_statement_new_line_after_left_paren = false
+ij_groovy_for_statement_right_paren_on_new_line = false
+ij_groovy_for_statement_wrap = off
+ij_groovy_if_brace_force = never
+ij_groovy_import_annotation_wrap = 2
+ij_groovy_imports_layout = *, |, javax.**, java.**, |, $*
+ij_groovy_indent_case_from_switch = true
+ij_groovy_indent_label_blocks = true
+ij_groovy_insert_inner_class_imports = false
+ij_groovy_keep_blank_lines_before_right_brace = 2
+ij_groovy_keep_blank_lines_in_code = 2
+ij_groovy_keep_blank_lines_in_declarations = 2
+ij_groovy_keep_control_statement_in_one_line = true
+ij_groovy_keep_first_column_comment = true
+ij_groovy_keep_indents_on_empty_lines = false
+ij_groovy_keep_line_breaks = true
+ij_groovy_keep_multiple_expressions_in_one_line = false
+ij_groovy_keep_simple_blocks_in_one_line = false
+ij_groovy_keep_simple_classes_in_one_line = true
+ij_groovy_keep_simple_lambdas_in_one_line = true
+ij_groovy_keep_simple_methods_in_one_line = true
+ij_groovy_label_indent_absolute = false
+ij_groovy_label_indent_size = 0
+ij_groovy_lambda_brace_style = end_of_line
+ij_groovy_layout_static_imports_separately = true
+ij_groovy_line_comment_add_space = false
+ij_groovy_line_comment_at_first_column = true
+ij_groovy_method_annotation_wrap = split_into_lines
+ij_groovy_method_brace_style = end_of_line
+ij_groovy_method_call_chain_wrap = off
+ij_groovy_method_parameters_new_line_after_left_paren = false
+ij_groovy_method_parameters_right_paren_on_new_line = false
+ij_groovy_method_parameters_wrap = off
+ij_groovy_modifier_list_wrap = false
+ij_groovy_names_count_to_use_import_on_demand = 3
+ij_groovy_parameter_annotation_wrap = off
+ij_groovy_parentheses_expression_new_line_after_left_paren = false
+ij_groovy_parentheses_expression_right_paren_on_new_line = false
+ij_groovy_prefer_parameters_wrap = false
+ij_groovy_resource_list_new_line_after_left_paren = false
+ij_groovy_resource_list_right_paren_on_new_line = false
+ij_groovy_resource_list_wrap = off
+ij_groovy_space_after_assert_separator = true
+ij_groovy_space_after_colon = true
+ij_groovy_space_after_comma = true
+ij_groovy_space_after_comma_in_type_arguments = true
+ij_groovy_space_after_for_semicolon = true
+ij_groovy_space_after_quest = true
+ij_groovy_space_after_type_cast = true
+ij_groovy_space_before_annotation_parameter_list = false
+ij_groovy_space_before_array_initializer_left_brace = false
+ij_groovy_space_before_assert_separator = false
+ij_groovy_space_before_catch_keyword = true
+ij_groovy_space_before_catch_left_brace = true
+ij_groovy_space_before_catch_parentheses = true
+ij_groovy_space_before_class_left_brace = true
+ij_groovy_space_before_closure_left_brace = true
+ij_groovy_space_before_colon = true
+ij_groovy_space_before_comma = false
+ij_groovy_space_before_do_left_brace = true
+ij_groovy_space_before_else_keyword = true
+ij_groovy_space_before_else_left_brace = true
+ij_groovy_space_before_finally_keyword = true
+ij_groovy_space_before_finally_left_brace = true
+ij_groovy_space_before_for_left_brace = true
+ij_groovy_space_before_for_parentheses = true
+ij_groovy_space_before_for_semicolon = false
+ij_groovy_space_before_if_left_brace = true
+ij_groovy_space_before_if_parentheses = true
+ij_groovy_space_before_method_call_parentheses = false
+ij_groovy_space_before_method_left_brace = true
+ij_groovy_space_before_method_parentheses = false
+ij_groovy_space_before_quest = true
+ij_groovy_space_before_switch_left_brace = true
+ij_groovy_space_before_switch_parentheses = true
+ij_groovy_space_before_synchronized_left_brace = true
+ij_groovy_space_before_synchronized_parentheses = true
+ij_groovy_space_before_try_left_brace = true
+ij_groovy_space_before_try_parentheses = true
+ij_groovy_space_before_while_keyword = true
+ij_groovy_space_before_while_left_brace = true
+ij_groovy_space_before_while_parentheses = true
+ij_groovy_space_in_named_argument = true
+ij_groovy_space_in_named_argument_before_colon = false
+ij_groovy_space_within_empty_array_initializer_braces = false
+ij_groovy_space_within_empty_method_call_parentheses = false
+ij_groovy_spaces_around_additive_operators = true
+ij_groovy_spaces_around_assignment_operators = true
+ij_groovy_spaces_around_bitwise_operators = true
+ij_groovy_spaces_around_equality_operators = true
+ij_groovy_spaces_around_lambda_arrow = true
+ij_groovy_spaces_around_logical_operators = true
+ij_groovy_spaces_around_multiplicative_operators = true
+ij_groovy_spaces_around_regex_operators = true
+ij_groovy_spaces_around_relational_operators = true
+ij_groovy_spaces_around_shift_operators = true
+ij_groovy_spaces_within_annotation_parentheses = false
+ij_groovy_spaces_within_array_initializer_braces = false
+ij_groovy_spaces_within_braces = true
+ij_groovy_spaces_within_brackets = false
+ij_groovy_spaces_within_cast_parentheses = false
+ij_groovy_spaces_within_catch_parentheses = false
+ij_groovy_spaces_within_for_parentheses = false
+ij_groovy_spaces_within_gstring_injection_braces = false
+ij_groovy_spaces_within_if_parentheses = false
+ij_groovy_spaces_within_list_or_map = false
+ij_groovy_spaces_within_method_call_parentheses = false
+ij_groovy_spaces_within_method_parentheses = false
+ij_groovy_spaces_within_parentheses = false
+ij_groovy_spaces_within_switch_parentheses = false
+ij_groovy_spaces_within_synchronized_parentheses = false
+ij_groovy_spaces_within_try_parentheses = false
+ij_groovy_spaces_within_tuple_expression = false
+ij_groovy_spaces_within_while_parentheses = false
+ij_groovy_special_else_if_treatment = true
+ij_groovy_ternary_operation_wrap = off
+ij_groovy_throws_keyword_wrap = off
+ij_groovy_throws_list_wrap = off
+ij_groovy_use_flying_geese_braces = false
+ij_groovy_use_fq_class_names = false
+ij_groovy_use_fq_class_names_in_javadoc = true
+ij_groovy_use_relative_indents = false
+ij_groovy_use_single_class_imports = true
+ij_groovy_variable_annotation_wrap = off
+ij_groovy_while_brace_force = never
+ij_groovy_while_on_new_line = false
+ij_groovy_wrap_long_lines = false
+
+[{*.gradle.kts, *.kt, *.kts, *.main.kts}]
+ij_kotlin_align_in_columns_case_branch = false
+ij_kotlin_align_multiline_binary_operation = false
+ij_kotlin_align_multiline_extends_list = false
+ij_kotlin_align_multiline_method_parentheses = false
+ij_kotlin_align_multiline_parameters = true
+ij_kotlin_align_multiline_parameters_in_calls = false
+ij_kotlin_allow_trailing_comma = false
+ij_kotlin_allow_trailing_comma_on_call_site = false
+ij_kotlin_assignment_wrap = off
+ij_kotlin_blank_lines_after_class_header = 0
+ij_kotlin_blank_lines_around_block_when_branches = 0
+ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
+ij_kotlin_block_comment_at_first_column = true
+ij_kotlin_call_parameters_new_line_after_left_paren = false
+ij_kotlin_call_parameters_right_paren_on_new_line = false
+ij_kotlin_call_parameters_wrap = off
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_class_annotation_wrap = split_into_lines
+ij_kotlin_continuation_indent_for_chained_calls = true
+ij_kotlin_continuation_indent_for_expression_bodies = true
+ij_kotlin_continuation_indent_in_argument_lists = true
+ij_kotlin_continuation_indent_in_elvis = true
+ij_kotlin_continuation_indent_in_if_conditions = true
+ij_kotlin_continuation_indent_in_parameter_lists = true
+ij_kotlin_continuation_indent_in_supertype_lists = true
+ij_kotlin_else_on_new_line = false
+ij_kotlin_enum_constants_wrap = off
+ij_kotlin_extends_list_wrap = off
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_finally_on_new_line = false
+ij_kotlin_if_rparen_on_new_line = false
+ij_kotlin_import_nested_classes = false
+ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
+ij_kotlin_keep_blank_lines_before_right_brace = 2
+ij_kotlin_keep_blank_lines_in_code = 2
+ij_kotlin_keep_blank_lines_in_declarations = 2
+ij_kotlin_keep_first_column_comment = true
+ij_kotlin_keep_indents_on_empty_lines = false
+ij_kotlin_keep_line_breaks = true
+ij_kotlin_lbrace_on_next_line = false
+ij_kotlin_line_comment_add_space = false
+ij_kotlin_line_comment_at_first_column = true
+ij_kotlin_method_annotation_wrap = split_into_lines
+ij_kotlin_method_call_chain_wrap = off
+ij_kotlin_method_parameters_new_line_after_left_paren = false
+ij_kotlin_method_parameters_right_paren_on_new_line = false
+ij_kotlin_method_parameters_wrap = off
+ij_kotlin_name_count_to_use_star_import = 5
+ij_kotlin_name_count_to_use_star_import_for_members = 3
+ij_kotlin_parameter_annotation_wrap = off
+ij_kotlin_space_after_comma = true
+ij_kotlin_space_after_extend_colon = true
+ij_kotlin_space_after_type_colon = true
+ij_kotlin_space_before_catch_parentheses = true
+ij_kotlin_space_before_comma = false
+ij_kotlin_space_before_extend_colon = true
+ij_kotlin_space_before_for_parentheses = true
+ij_kotlin_space_before_if_parentheses = true
+ij_kotlin_space_before_lambda_arrow = true
+ij_kotlin_space_before_type_colon = false
+ij_kotlin_space_before_when_parentheses = true
+ij_kotlin_space_before_while_parentheses = true
+ij_kotlin_spaces_around_additive_operators = true
+ij_kotlin_spaces_around_assignment_operators = true
+ij_kotlin_spaces_around_equality_operators = true
+ij_kotlin_spaces_around_function_type_arrow = true
+ij_kotlin_spaces_around_logical_operators = true
+ij_kotlin_spaces_around_multiplicative_operators = true
+ij_kotlin_spaces_around_range = false
+ij_kotlin_spaces_around_relational_operators = true
+ij_kotlin_spaces_around_unary_operator = false
+ij_kotlin_spaces_around_when_arrow = true
+ij_kotlin_variable_annotation_wrap = off
+ij_kotlin_while_on_new_line = false
+ij_kotlin_wrap_elvis_expressions = 1
+ij_kotlin_wrap_expression_body_functions = 0
+ij_kotlin_wrap_first_method_in_call_chain = false
+ktlint_standard_filename = disabled
+
+[{*.har, *.jsb2, *.jsb3, *.json, .babelrc, .eslintrc, .stylelintrc, bowerrc, jest.config, mcmod.info}]
+indent_size = 2
+ij_json_keep_blank_lines_in_code = 0
+ij_json_keep_indents_on_empty_lines = false
+ij_json_keep_line_breaks = true
+ij_json_space_after_colon = true
+ij_json_space_after_comma = true
+ij_json_space_before_colon = true
+ij_json_space_before_comma = false
+ij_json_spaces_within_braces = false
+ij_json_spaces_within_brackets = false
+ij_json_wrap_long_lines = false
+
+[{*.htm, *.html, *.sht, *.shtm, *.shtml}]
+ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3
+ij_html_align_attributes = true
+ij_html_align_text = false
+ij_html_attribute_wrap = normal
+ij_html_block_comment_at_first_column = true
+ij_html_do_not_align_children_of_min_lines = 0
+ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p
+ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot
+ij_html_enforce_quotes = false
+ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var
+ij_html_keep_blank_lines = 2
+ij_html_keep_indents_on_empty_lines = false
+ij_html_keep_line_breaks = true
+ij_html_keep_line_breaks_in_text = true
+ij_html_keep_whitespaces = false
+ij_html_keep_whitespaces_inside = span, pre, textarea
+ij_html_line_comment_at_first_column = true
+ij_html_new_line_after_last_attribute = never
+ij_html_new_line_before_first_attribute = never
+ij_html_quote_style = double
+ij_html_remove_new_line_before_tags = br
+ij_html_space_after_tag_name = false
+ij_html_space_around_equality_in_attribute = false
+ij_html_space_inside_empty_tag = false
+ij_html_text_wrap = normal
+ij_html_uniform_ident = false
+
+[{*.yaml, *.yml}]
+indent_size = 2
+ij_yaml_keep_indents_on_empty_lines = false
+ij_yaml_keep_line_breaks = true
+ij_yaml_space_before_colon = true
+ij_yaml_spaces_within_braces = true
+ij_yaml_spaces_within_brackets = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..022b841
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# These are explicitly windows files and should use crlf
+*.bat text eol=crlf
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..c6cb0c8
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: Citymonstret
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: [ 'https://paypal.me/Sauilitired' ]# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..e27ac67
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,49 @@
+name: Build cloud-discord
+on:
+ push:
+ branches: [ "**" ]
+ tags-ignore: [ "**" ]
+ pull_request:
+ release:
+ types: [ released ]
+jobs:
+ build:
+ # Only run on PRs if the source branch is on someone else's repo
+ if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }}
+ runs-on: "ubuntu-latest"
+ steps:
+ - uses: actions/checkout@v4
+ - uses: gradle/wrapper-validation-action@v1
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: 17
+ - name: Build
+ run: ./gradlew build
+ - name : Test Summary
+ uses : EnricoMi/publish-unit-test-result-action@v2.11.0
+ with :
+ junit_files : "**/build/test-results/test/TEST-*.xml"
+ if : always()
+ - name: Determine Status
+ run: |
+ if [ "$(./gradlew properties | awk '/^version:/ { print $2; }' | grep '\-SNAPSHOT')" ]; then
+ echo "STATUS=snapshot" >> $GITHUB_ENV
+ else
+ echo "STATUS=release" >> $GITHUB_ENV
+ fi
+ - name: Publish Snapshot
+ if: "${{ env.STATUS != 'release' && github.event_name == 'push' && github.ref == 'refs/heads/1.0.0-dev' }}"
+ run: ./gradlew publish
+ env:
+ ORG_GRADLE_PROJECT_sonatypeUsername: "${{ secrets.SONATYPE_USERNAME }}"
+ ORG_GRADLE_PROJECT_sonatypePassword: "${{ secrets.SONATYPE_PASSWORD }}"
+ - name: Publish Release
+ if: "${{ env.STATUS == 'release' && github.event_name == 'release' }}"
+ run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
+ env:
+ ORG_GRADLE_PROJECT_sonatypeUsername: "${{ secrets.SONATYPE_USERNAME }}"
+ ORG_GRADLE_PROJECT_sonatypePassword: "${{ secrets.SONATYPE_PASSWORD }}"
+ ORG_GRADLE_PROJECT_signingKey: "${{ secrets.SIGNING_KEY }}"
+ ORG_GRADLE_PROJECT_signingPassword: "${{ secrets.SIGNING_PASSWORD }}"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aaedbd7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,190 @@
+# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,java,maven,gradle,kotlin,git
+# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,java,maven,gradle,kotlin,git
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### Intellij+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij+all Patch ###
+# Ignore everything but code style settings and run configurations
+# that are supposed to be shared within teams.
+
+.idea/*
+
+!.idea/codeStyles
+!.idea/runConfigurations
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Kotlin ###
+# Compiled class file
+
+# Log file
+
+# BlueJ files
+
+# Mobile Tools for Java (J2ME)
+
+# Package Files #
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+# Eclipse m2e generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+### Gradle ###
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+# JDT-specific (Eclipse Java Development Tools)
+
+### Gradle Patch ###
+# Java heap dump
+*.hprof
+
+# End of https://www.toptal.com/developers/gitignore/api/intellij+all,java,maven,gradle,kotlin,git
\ No newline at end of file
diff --git a/.spotless/cloud-discord.importorder b/.spotless/cloud-discord.importorder
new file mode 100644
index 0000000..3eab772
--- /dev/null
+++ b/.spotless/cloud-discord.importorder
@@ -0,0 +1,3 @@
+# cloud-discord import order
+0=
+1=\#
diff --git a/HEADER b/HEADER
new file mode 100644
index 0000000..04f33e4
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,23 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ac51a62
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Incendo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5c94f84
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# cloud-discord
+
+Discord integrations for [Cloud v2](https://github.com/incendo/cloud).
+
+## modules
+
+- cloud-discord-common: shared utilities
+- cloud-jda5: integration for JDA5 slash commands
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..4dd4319
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+ alias(libs.plugins.cloud.buildLogic.rootProject.publishing)
+ alias(libs.plugins.cloud.buildLogic.rootProject.spotless)
+}
+
+spotlessPredeclare {
+ kotlin { ktlint(libs.versions.ktlint.get()) }
+ kotlinGradle { ktlint(libs.versions.ktlint.get()) }
+}
+
+subprojects {
+ afterEvaluate {
+ tasks.withType().configureEach {
+ options.compilerArgs.remove("-Werror")
+ }
+ }
+}
+
+tasks {
+ spotlessCheck {
+ dependsOn(gradle.includedBuild("build-logic").task(":spotlessCheck") )
+ }
+ spotlessApply {
+ dependsOn(gradle.includedBuild("build-logic").task(":spotlessApply"))
+ }
+}
diff --git a/cloud-discord-common/README.md b/cloud-discord-common/README.md
new file mode 100644
index 0000000..c450115
--- /dev/null
+++ b/cloud-discord-common/README.md
@@ -0,0 +1,7 @@
+# cloud-discord-common
+
+Shared utilities for Discord integrations.
+
+## slash commands
+
+cloud-discord-common contains tooling for mapping Cloud commands to Discord commands.
diff --git a/cloud-discord-common/build.gradle.kts b/cloud-discord-common/build.gradle.kts
new file mode 100644
index 0000000..614af4a
--- /dev/null
+++ b/cloud-discord-common/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id("cloud-discord.base-conventions")
+ id("cloud-discord.publishing-conventions")
+}
+
+dependencies {
+ api(libs.cloud.core)
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/ImmutableImpl.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/ImmutableImpl.java
new file mode 100644
index 0000000..b7e84d3
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/ImmutableImpl.java
@@ -0,0 +1,55 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.immutables;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apiguardian.api.API;
+import org.immutables.value.Value;
+
+/**
+ * Annotation that generates immutable classes suffixed with "Impl".
+ */
+@Value.Style(
+ typeImmutable = "*Impl",
+ typeImmutableEnclosing = "*",
+ typeAbstract = "*",
+ deferCollectionAllocation = true,
+ optionalAcceptNullable = true,
+ jdkOnly = true, // We do not want any runtime dependencies!
+ allParameters = true,
+ headerComments = true,
+ jacksonIntegration = false,
+ visibility = Value.Style.ImplementationVisibility.PACKAGE,
+ builderVisibility = Value.Style.BuilderVisibility.PACKAGE,
+ defaults = @Value.Immutable(builder = false)
+)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PACKAGE})
+@Retention(RetentionPolicy.SOURCE)
+@API(status = API.Status.INTERNAL, since = "1.0.0")
+public @interface ImmutableImpl {
+
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/StagedImmutableBuilder.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/StagedImmutableBuilder.java
new file mode 100644
index 0000000..a83571b
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/StagedImmutableBuilder.java
@@ -0,0 +1,58 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.immutables;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apiguardian.api.API;
+import org.immutables.value.Value;
+
+/**
+ * Annotation that generates immutables classes with staged builders.
+ */
+@Value.Style(
+ typeImmutableEnclosing = "*",
+ typeAbstract = "*",
+ deferCollectionAllocation = true,
+ optionalAcceptNullable = true,
+ jdkOnly = true, // We do not want any runtime dependencies!
+ allParameters = true,
+ headerComments = true,
+ jacksonIntegration = false,
+ builderVisibility = Value.Style.BuilderVisibility.SAME,
+ defaultAsDefault = true,
+ put = "*",
+ putAll = "*",
+ stagedBuilder = true,
+ depluralize = true,
+ depluralizeDictionary = "creationListeners:creationListener"
+)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PACKAGE})
+@Retention(RetentionPolicy.SOURCE)
+@API(status = API.Status.INTERNAL, since = "1.0.0")
+public @interface StagedImmutableBuilder {
+
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/package-info.java
new file mode 100644
index 0000000..65a34e8
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Configurations for {@link org.immutables.value.Value.Immutable}.
+ */
+package org.incendo.cloud.discord.immutables;
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/package-info.java
new file mode 100644
index 0000000..273df8c
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/package-info.java
@@ -0,0 +1 @@
+package org.incendo.cloud.discord;
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/CommandScope.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/CommandScope.java
new file mode 100644
index 0000000..1c70553
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/CommandScope.java
@@ -0,0 +1,183 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.Command;
+import cloud.commandframework.keys.CloudKey;
+import io.leangen.geantyref.TypeToken;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface CommandScope extends Command.Builder.Applicable {
+
+ CloudKey> META_COMMAND_SCOPE = CloudKey.of(
+ "cloud:command_scope",
+ new TypeToken>() {
+ }
+ );
+
+ /**
+ * Returns the global command scope.
+ *
+ * @param command sender type
+ * @return global scope
+ */
+ @SuppressWarnings("unchecked")
+ static @NonNull CommandScope global() {
+ return (CommandScope) Global.GLOBAL;
+ }
+
+ /**
+ * Returns a command scope for the given {@code guilds}.
+ *
+ * @param command sender type
+ * @param guilds the guilds
+ * @return guild scope
+ */
+ static @NonNull CommandScope guilds(final long @NonNull... guilds) {
+ return new Guilds<>(guilds);
+ }
+
+ /**
+ * Returns a command scope that makes the command be active in all guilds.
+ *
+ * @param command sender type
+ * @return guild scope
+ */
+ static @NonNull CommandScope guilds() {
+ return new Guilds<>(-1);
+ }
+
+ /**
+ * Checks if there's any overlap between {@code this} scope and the given {@code scope}.
+ *
+ * @param scope other scope
+ * @return {@code true} if the scopes overlap, else {@code false}
+ */
+ boolean overlaps(@NonNull CommandScope scope);
+
+ @Override
+ default Command.@NonNull Builder applyToCommandBuilder(Command.@NonNull Builder builder) {
+ return builder.meta(META_COMMAND_SCOPE, this);
+ }
+
+ /**
+ * Makes the command globally available.
+ *
+ * @param command sender type
+ * @since 1.0.0
+ */
+ @API(status = API.Status.STABLE, since = "1.0.0")
+ final class Global implements CommandScope {
+
+ private static final Global> GLOBAL = new Global<>();
+
+ private Global() {
+ }
+
+ @Override
+ public boolean overlaps(final @NonNull CommandScope scope) {
+ return true;
+ }
+ }
+
+ /**
+ * Makes the command available in specific guilds.
+ *
+ * @param command sender type
+ * @since 1.0.0
+ */
+ @API(status = API.Status.STABLE, since = "1.0.0")
+ final class Guilds implements CommandScope {
+
+ private final Set guilds;
+
+ private Guilds(final @NonNull Set<@NonNull Long> guilds) {
+ this.guilds = Collections.unmodifiableSet(guilds);
+ }
+
+ private Guilds(final long @NonNull... guilds) {
+ this(Arrays.stream(guilds).boxed().collect(Collectors.toSet()));
+ }
+
+ private Guilds(final long guildId) {
+ this.guilds = Collections.singleton(guildId);
+ }
+
+ /**
+ * Returns an unmodifiable view of the guilds.
+ *
+ * @return the guilds
+ */
+ public @NonNull Set<@NonNull Long> guilds() {
+ return this.guilds;
+ }
+
+ /**
+ * Returns a new {@link Guilds} instance with the given {@code guildId} added.
+ *
+ * @param guildId guild to add
+ * @return the new instance
+ */
+ public @NonNull Guilds withGuild(final long guildId) {
+ final Set guilds = new HashSet<>(this.guilds);
+ guilds.add(guildId);
+ return new Guilds<>(guilds);
+ }
+
+ /**
+ * Returns a new {@link Guilds} instance with the given {@code guilds} added.
+ *
+ * @param guilds new guilds to add
+ * @return the new instance
+ */
+ public @NonNull Guilds withGuild(final Set<@NonNull Long> guilds) {
+ final Set newGuilds = new HashSet<>(this.guilds);
+ newGuilds.addAll(guilds);
+ return new Guilds<>(newGuilds);
+ }
+
+ @Override
+ public boolean overlaps(final @NonNull CommandScope scope) {
+ if (!(scope instanceof Guilds)) {
+ return false;
+ }
+ final Guilds guilds = (Guilds) scope;
+ return this.guilds.stream().anyMatch(guildId -> guilds.guilds().contains(guildId));
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "Guilds{"
+ + "guilds=" + this.guilds
+ + '}';
+ }
+ }
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoiceProvider.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoiceProvider.java
new file mode 100644
index 0000000..6f3ff8b
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoiceProvider.java
@@ -0,0 +1,52 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.arguments.suggestion.BlockingSuggestionProvider;
+import cloud.commandframework.arguments.suggestion.Suggestion;
+import cloud.commandframework.context.CommandContext;
+import cloud.commandframework.context.CommandInput;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface DiscordChoiceProvider extends BlockingSuggestionProvider {
+
+ @Override
+ default @NonNull Iterable<@NonNull Suggestion> suggestions(
+ final @NonNull CommandContext context,
+ final @NonNull CommandInput input
+ ) {
+ return new ArrayList<>(this.choices());
+ }
+
+ /**
+ * Returns the choices.
+ *
+ * @return the choices
+ */
+ @NonNull Collection<@NonNull DiscordOptionChoice> choices();
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoices.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoices.java
new file mode 100644
index 0000000..bd4bce1
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoices.java
@@ -0,0 +1,157 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.immutables.value.Value;
+import org.incendo.cloud.discord.immutables.ImmutableImpl;
+
+@ImmutableImpl
+@Value.Immutable
+@SuppressWarnings("varargs")
+@API(status = API.Status.STABLE, since = "1.0.0 ")
+public interface DiscordChoices extends DiscordChoiceProvider {
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choice type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices choices(final @NonNull Collection<@NonNull DiscordOptionChoice> choices) {
+ return DiscordChoicesImpl.of(choices);
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choice type
+ * @param choices choices
+ * @return the created instance
+ */
+ @SafeVarargs
+ static @NonNull DiscordChoices choices(final @NonNull DiscordOptionChoice @NonNull... choices) {
+ return DiscordChoicesImpl.of(Arrays.asList(choices));
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices integers(final @NonNull Collection<@NonNull Integer> choices) {
+ return DiscordChoicesImpl.of(
+ choices.stream()
+ .map(integer -> DiscordOptionChoice.of(Integer.toString(integer), integer))
+ .collect(Collectors.toList())
+ );
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices integers(final int @NonNull... choices) {
+ return DiscordChoicesImpl.of(
+ Arrays.stream(choices)
+ .mapToObj(integer -> DiscordOptionChoice.of(Integer.toString(integer), integer))
+ .collect(Collectors.toList())
+ );
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices doubles(final @NonNull Collection<@NonNull Double> choices) {
+ return DiscordChoicesImpl.of(
+ choices.stream()
+ .map(number -> DiscordOptionChoice.of(Double.toString(number), number))
+ .collect(Collectors.toList())
+ );
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices doubles(final double @NonNull... choices) {
+ return DiscordChoicesImpl.of(
+ Arrays.stream(choices)
+ .mapToObj(number -> DiscordOptionChoice.of(Double.toString(number), number))
+ .collect(Collectors.toList())
+ );
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices strings(final @NonNull Collection<@NonNull String> choices) {
+ return DiscordChoicesImpl.of(
+ choices.stream()
+ .map(string -> DiscordOptionChoice.of(string, string))
+ .collect(Collectors.toList())
+ );
+ }
+
+ /**
+ * Creates a {@link DiscordChoices} instance from the given {@code choices}.
+ *
+ * @param command sender type
+ * @param choices choices
+ * @return the created instance
+ */
+ static @NonNull DiscordChoices strings(final @NonNull String @NonNull... choices) {
+ return DiscordChoicesImpl.of(
+ Arrays.stream(choices)
+ .map(string -> DiscordOptionChoice.of(string, string))
+ .collect(Collectors.toList())
+ );
+ }
+
+ @Override
+ @NonNull Collection<@NonNull DiscordOptionChoice> choices();
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommand.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommand.java
new file mode 100644
index 0000000..eca8184
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommand.java
@@ -0,0 +1,74 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import java.util.List;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.immutables.value.Value;
+import org.incendo.cloud.discord.immutables.StagedImmutableBuilder;
+
+/**
+ * Represents a Discord command.
+ *
+ * @param command sender type
+ * @since 1.0.0
+ */
+@StagedImmutableBuilder
+@Value.Immutable
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface DiscordCommand extends Named {
+
+ /**
+ * Creates a new command builder.
+ *
+ * @param command sender type
+ * @return the builder
+ */
+ static ImmutableDiscordCommand.@NonNull NameBuildStage builder() {
+ return ImmutableDiscordCommand.builder();
+ }
+
+ /**
+ * Returns the command name.
+ *
+ * @return the name
+ */
+ @Override
+ @NonNull String name();
+
+ /**
+ * Returns the command description.
+ *
+ * @return the description
+ */
+ @NonNull String description();
+
+ /**
+ * Returns the command options.
+ *
+ * @return the options
+ */
+ @NonNull List<@NonNull DiscordOption> options();
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommandFactory.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommandFactory.java
new file mode 100644
index 0000000..31d4dca
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommandFactory.java
@@ -0,0 +1,40 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.internal.CommandNode;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface DiscordCommandFactory {
+
+ /**
+ * Creates the command that represent the given {@code node}.
+ *
+ * @param node cloud node
+ * @return the option
+ */
+ @NonNull DiscordCommand create(@NonNull CommandNode node);
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOption.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOption.java
new file mode 100644
index 0000000..68585bb
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOption.java
@@ -0,0 +1,136 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.Command;
+import java.util.List;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+import org.incendo.cloud.discord.immutables.StagedImmutableBuilder;
+
+/**
+ * Represents a Discord option.
+ *
+ * @param command sender type
+ * @since 1.0.0
+ */
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface DiscordOption extends Named {
+
+ /**
+ * Returns the option name.
+ *
+ *
Must be between 1 and 32 characters.
+ *
+ * @return the name
+ */
+ @Override
+ @NonNull String name();
+
+ /**
+ * Returns the option description.
+ *
+ *
Must be between 1 and 100 characters.
+ *
+ * @return the description
+ */
+ @NonNull String description();
+
+ /**
+ * Returns the type of the option.
+ *
+ * @return option type
+ */
+ @NonNull DiscordOptionType> type();
+
+ /**
+ * Returns the associated command.
+ *
+ * @return the command, or {@code null}
+ */
+ @Nullable Command command();
+
+
+ @SuppressWarnings("immutables:subtype")
+ @StagedImmutableBuilder
+ @Value.Immutable
+ interface SubCommand extends DiscordOption {
+
+ @Value.Derived
+ @Override
+ default @NonNull DiscordOptionType> type() {
+ if (!this.options().isEmpty() && this.options().get(0) instanceof SubCommand) {
+ return DiscordOptionType.SUB_COMMAND_GROUP;
+ }
+ return DiscordOptionType.SUB_COMMAND;
+ }
+
+ /**
+ * Returns the child options.
+ *
+ * @return the options
+ */
+ @NonNull List<@NonNull DiscordOption> options();
+ }
+
+ @SuppressWarnings("immutables:subtype")
+ @StagedImmutableBuilder
+ @Value.Immutable
+ interface Variable extends DiscordOption {
+
+ /**
+ * Whether the option is required.
+ *
+ * @return whether the option is required
+ */
+ boolean required();
+
+ /**
+ * Whether the autocomplete interaction is enabled for this option.
+ *
+ * @return whether autocomplete is enabled
+ */
+ boolean autocomplete();
+
+ /**
+ * Returns the choices.
+ *
+ *
This is only relevant for STRING, INTEGER & NUMBER options.
+ *
+ * @return the choices
+ */
+ @NonNull List<@NonNull DiscordOptionChoice>> choices();
+
+ /**
+ * Returns the range, if applicable.
+ *
+ * @return the range, or {@code null}
+ */
+ default @Nullable Range range() {
+ return null;
+ }
+ }
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionChoice.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionChoice.java
new file mode 100644
index 0000000..2c49ad4
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionChoice.java
@@ -0,0 +1,79 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.arguments.suggestion.Suggestion;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.immutables.value.Value;
+import org.incendo.cloud.discord.immutables.ImmutableImpl;
+
+/**
+ * Represents a Discord option choice.
+ *
+ * @param option type
+ * @since 1.0.0
+ */
+@ImmutableImpl
+@Value.Immutable
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface DiscordOptionChoice extends Named, Suggestion {
+
+ /**
+ * Creates a new choice.
+ *
+ * @param option type
+ * @param name choice name
+ * @param value choice value
+ * @return the choice
+ */
+ static @NonNull DiscordOptionChoice of(final @NonNull String name, final @NonNull T value) {
+ return DiscordOptionChoiceImpl.of(name, value);
+ }
+
+ /**
+ * Returns the choice name.
+ *
+ * @return the name
+ */
+ @Override
+ @NonNull String name();
+
+ /**
+ * Returns the choice value.
+ *
+ * @return the value
+ */
+ @NonNull T value();
+
+ @Override
+ default @NonNull String suggestion() {
+ return this.name();
+ }
+
+ @Override
+ default @NonNull DiscordOptionChoice withSuggestion(final @NonNull String suggestion) {
+ return DiscordOptionChoiceImpl.copyOf(this).withSuggestion(suggestion);
+ }
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionType.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionType.java
new file mode 100644
index 0000000..a529cd2
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionType.java
@@ -0,0 +1,101 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import io.leangen.geantyref.TypeToken;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.immutables.value.Value;
+import org.incendo.cloud.discord.immutables.ImmutableImpl;
+
+/**
+ * Represents a Discord option type.
+ *
+ * @param option type
+ * @since 1.0.0
+ */
+@Value.Immutable
+@ImmutableImpl
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface DiscordOptionType<@NonNull T> extends Named {
+
+ @NonNull DiscordOptionType> SUB_COMMAND = of("SUB_COMMAND", 1, new TypeToken>() {
+ });
+ @NonNull DiscordOptionType>> SUB_COMMAND_GROUP = of("SUB_COMMAND_GROUP", 2,
+ new TypeToken>>() {
+ }
+ );
+ @NonNull DiscordOptionType STRING = of("STRING", 3, TypeToken.get(String.class));
+ @NonNull DiscordOptionType INTEGER = of("INTEGER", 4, TypeToken.get(Integer.class));
+ @NonNull DiscordOptionType BOOLEAN = of("BOOLEAN", 5, TypeToken.get(Boolean.class));
+ @NonNull DiscordOptionType NUMBER = of("NUMBER", 10, TypeToken.get(Double.class));
+ // Non-generic types must be implemented in the platform modules.
+
+ @NonNull Collection<@NonNull DiscordOptionType>> AUTOCOMPLETE = Collections.unmodifiableCollection(
+ Arrays.asList(STRING, INTEGER, NUMBER)
+ );
+
+ /**
+ * Creates a new option instance.
+ *
+ * @param type represented by the option
+ * @param name option name
+ * @param value option value
+ * @param type option type
+ * @return the option
+ */
+ static @NonNull DiscordOptionType of(
+ final @NonNull String name,
+ final int value,
+ final TypeToken type
+ ) {
+ return DiscordOptionTypeImpl.of(name, value, type);
+ }
+
+ /**
+ * Returns the name of the option type.
+ *
+ * @return option type name
+ */
+ @Override
+ @NonNull String name();
+
+ /**
+ * Returns the option value.
+ *
+ * @return option value
+ */
+ int value();
+
+ /**
+ * Returns the option type.
+ *
+ * @return option type
+ */
+ @NonNull TypeToken type();
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordPermission.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordPermission.java
new file mode 100644
index 0000000..541084e
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordPermission.java
@@ -0,0 +1,72 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.permission.Permission;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+@API(status = API.Status.STABLE, since = "1.0.0")
+public final class DiscordPermission implements Permission {
+
+ /**
+ * Creates a new Discord permission.
+ *
+ * @param permission permission
+ * @return the created permission
+ */
+ public static @NonNull DiscordPermission of(final long permission) {
+ return new DiscordPermission(permission);
+ }
+
+ /**
+ * Creates a new Discord permission.
+ *
+ * @param permission permission
+ * @return the created permission
+ */
+ public static @NonNull DiscordPermission discordPermission(final long permission) {
+ return new DiscordPermission(permission);
+ }
+
+ private final long permission;
+
+ private DiscordPermission(final long permission) {
+ this.permission = permission;
+ }
+
+ @Override
+ public @NonNull String permissionString() {
+ return Long.toString(this.permission);
+ }
+
+ /**
+ * Returns the permission.
+ *
+ * @return permission
+ */
+ public long permission() {
+ return this.permission;
+ }
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordSetting.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordSetting.java
new file mode 100644
index 0000000..00959a1
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordSetting.java
@@ -0,0 +1,52 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.setting.Setting;
+import org.apiguardian.api.API;
+
+/**
+ * Discord-specific settings.
+ *
+ * @since 1.0.0
+ */
+@API(status = API.Status.STABLE, since = "1.0.0")
+public enum DiscordSetting implements Setting {
+ /**
+ * Whether slash commands should be registered automatically.
+ */
+ AUTO_REGISTER_SLASH_COMMANDS,
+ /**
+ * Whether error messages should be ephemeral.
+ */
+ EPHEMERAL_ERROR_MESSAGES,
+ /**
+ * Always defer replies.
+ */
+ FORCE_DEFER_NON_EPHEMERAL,
+ /**
+ * Always defer replies.
+ */
+ FORCE_DEFER_EPHEMERAL
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Named.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Named.java
new file mode 100644
index 0000000..15932d7
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Named.java
@@ -0,0 +1,43 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * Something with a name.
+ *
+ * @since 1.0.0
+ */
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface Named {
+
+ /**
+ * Returns the name.
+ *
+ * @return the name
+ */
+ @NonNull String name();
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/NodeProcessor.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/NodeProcessor.java
new file mode 100644
index 0000000..0daf2a3
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/NodeProcessor.java
@@ -0,0 +1,113 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.CommandTree;
+import cloud.commandframework.internal.CommandNode;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Processes {@link CommandNode nodes} and prepares them for mapping to Discord commands.
+ *
+ * @param command sender type
+ * @since 1.0.0
+ */
+@API(status = API.Status.INTERNAL, since = "1.0.0")
+public final class NodeProcessor {
+
+ public static final String NODE_META_SCOPE = "scope";
+
+ private final CommandTree commandTree;
+
+ /**
+ * Creates a new node processor.
+ *
+ * @param commandTree tree that should be processed
+ */
+ public NodeProcessor(final @NonNull CommandTree commandTree) {
+ this.commandTree = commandTree;
+ }
+
+ /**
+ * Prepares the command tree.
+ */
+ public void prepareTree() {
+ this.commandTree.getLeavesRaw(this.commandTree.rootNode())
+ .forEach(this::propagateRequirements);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void propagateRequirements(final @NonNull CommandNode leafNode) {
+ CommandScope parentScope = (CommandScope) leafNode.command().commandMeta().getOrDefault(
+ CommandScope.META_COMMAND_SCOPE,
+ CommandScope.global()
+ );
+ leafNode.nodeMeta().put(NODE_META_SCOPE, parentScope);
+
+ List> chain = this.getChain(leafNode);
+ Collections.reverse(chain);
+ chain = chain.subList(1, chain.size());
+
+ for (final CommandNode commandNode : chain) {
+ final CommandScope existingScope = (CommandScope) commandNode.nodeMeta().get(NODE_META_SCOPE);
+
+ CommandScope scope;
+ if (existingScope != null) {
+ // Global scope overrides everything :)
+ if (existingScope instanceof CommandScope.Global) {
+ scope = existingScope;
+ } else if (existingScope instanceof CommandScope.Guilds) {
+ if (parentScope instanceof CommandScope.Guilds) {
+ scope = ((CommandScope.Guilds) existingScope)
+ .withGuild(((CommandScope.Guilds) parentScope).guilds());
+ } else {
+ scope = existingScope;
+ }
+ } else {
+ scope = parentScope;
+ }
+ } else {
+ scope = parentScope;
+ }
+
+ commandNode.nodeMeta().put(NODE_META_SCOPE, scope);
+ }
+ }
+
+ private @NonNull List<@NonNull CommandNode> getChain(final @Nullable CommandNode end) {
+ final List> chain = new LinkedList<>();
+ CommandNode tail = end;
+ while (tail != null) {
+ chain.add(tail);
+ tail = tail.parent();
+ }
+ Collections.reverse(chain);
+ return chain;
+ }
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/OptionRegistry.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/OptionRegistry.java
new file mode 100644
index 0000000..b514038
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/OptionRegistry.java
@@ -0,0 +1,97 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.arguments.parser.ParserDescriptor;
+import io.leangen.geantyref.TypeToken;
+import java.util.Collection;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.common.returnsreceiver.qual.This;
+
+/**
+ * Registry that stores {@link DiscordOptionType}-{@link ParserDescriptor} mappings.
+ *
+ * @param command sender type
+ * @since 1.0.0
+ */
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface OptionRegistry {
+
+ /**
+ * Registers a mapping between the given {@code optionType} and the given {@code parser}.
+ *
+ * @param optionType discord option type
+ * @param parser cloud parser
+ * @return {@code this}
+ */
+ @This @NonNull OptionRegistry registerMapping(
+ @NonNull DiscordOptionType> optionType,
+ @NonNull ParserDescriptor parser
+ );
+
+ /**
+ * Returns the option type that best corresponds to the given {@code valueType}.
+ *
+ * @param valueType type to get the option for
+ * @return the best matching option, using {@link DiscordOptionType#STRING} as the fallback
+ */
+ @NonNull DiscordOptionType> getOption(@NonNull TypeToken> valueType);
+
+ /**
+ * Returns an unmodifiable view of the recognized option types.
+ *
+ * @return option types
+ */
+ @NonNull Collection<@NonNull DiscordOptionType>> optionTypes();
+
+ /**
+ * Returns the option type with the given {@code value}, if it exists.
+ *
+ * @param value value to get the option by
+ * @return the option type, or {@code null}
+ */
+ default @Nullable DiscordOptionType> getByValue(final int value) {
+ return this.optionTypes()
+ .stream()
+ .filter(option -> option.value() == value)
+ .findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * Returns the option type with the given {@code name}, if it exists.
+ *
+ * @param name name to get the option by
+ * @return the option type, or {@code null}
+ */
+ default @Nullable DiscordOptionType> getByName(final @NonNull String name) {
+ return this.optionTypes()
+ .stream()
+ .filter(option -> option.name().equalsIgnoreCase(name))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Range.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Range.java
new file mode 100644
index 0000000..cfa3a7d
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Range.java
@@ -0,0 +1,60 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.immutables.value.Value;
+import org.incendo.cloud.discord.immutables.ImmutableImpl;
+
+@ImmutableImpl
+@Value.Immutable
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface Range {
+
+ /**
+ * Creates a new range.
+ *
+ * @param min minimum value
+ * @param max maximum value
+ * @return the range
+ */
+ static @NonNull Range of(final @NonNull Number min, final @NonNull Number max) {
+ return RangeImpl.of(min, max);
+ }
+
+ /**
+ * Returns the minimum value.
+ *
+ * @return the min value
+ */
+ @NonNull Number min();
+
+ /**
+ * Returns the maximum value.
+ *
+ * @return the max value
+ */
+ @NonNull Number max();
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/RangeMapper.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/RangeMapper.java
new file mode 100644
index 0000000..89e71df
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/RangeMapper.java
@@ -0,0 +1,49 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.arguments.parser.ArgumentParser;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Maps min-max values to a {@link Range}.
+ *
+ * @param command sender type
+ * @param parser value type
+ * @param
parser type
+ * @since 1.0.0
+ */
+@API(status = API.Status.STABLE, since = "1.0.0")
+public interface RangeMapper> {
+
+ /**
+ * Maps the given {@code parser} to a range.
+ *
+ * @param parser parser to map
+ * @return the range, or {@code null} if no range could be extracted
+ */
+ @Nullable Range map(@NonNull P parser);
+}
diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardDiscordCommandFactory.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardDiscordCommandFactory.java
new file mode 100644
index 0000000..875a642
--- /dev/null
+++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardDiscordCommandFactory.java
@@ -0,0 +1,227 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.discord.slash;
+
+import cloud.commandframework.CommandComponent;
+import cloud.commandframework.arguments.parser.ArgumentParser;
+import cloud.commandframework.arguments.standard.ByteParser;
+import cloud.commandframework.arguments.standard.DoubleParser;
+import cloud.commandframework.arguments.standard.FloatParser;
+import cloud.commandframework.arguments.standard.IntegerParser;
+import cloud.commandframework.arguments.standard.LongParser;
+import cloud.commandframework.arguments.standard.ShortParser;
+import cloud.commandframework.arguments.suggestion.SuggestionProvider;
+import cloud.commandframework.internal.CommandNode;
+import io.leangen.geantyref.GenericTypeReflector;
+import io.leangen.geantyref.TypeToken;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+@API(status = API.Status.INTERNAL, since = "1.0.0")
+public class StandardDiscordCommandFactory implements DiscordCommandFactory {
+
+ private final OptionRegistry optionRegistry;
+ private final Map, RangeMapper> rangeMappers = new HashMap<>();
+
+ /**
+ * Creates a new factory instance.
+ *
+ * @param optionRegistry option registry to retrieve option types from
+ */
+ public StandardDiscordCommandFactory(final @NonNull OptionRegistry optionRegistry) {
+ this.optionRegistry = optionRegistry;
+ this.registerRangeMapper(new TypeToken>() {
+ }, parser -> {
+ if (!parser.hasMin() && !parser.hasMax()) {
+ return null;
+ }
+ return Range.of(parser.min(), parser.max());
+ });
+ this.registerRangeMapper(new TypeToken>() {
+ }, parser -> {
+ if (!parser.hasMin() && !parser.hasMax()) {
+ return null;
+ }
+ return Range.of(parser.min(), parser.max());
+ });
+ this.registerRangeMapper(new TypeToken>() {
+ }, parser -> {
+ if (!parser.hasMin() && !parser.hasMax()) {
+ return null;
+ }
+ return Range.of(parser.min(), parser.max());
+ });
+ this.registerRangeMapper(new TypeToken>() {
+ }, parser -> {
+ if (!parser.hasMin() && !parser.hasMax()) {
+ return null;
+ }
+ return Range.of(parser.min(), parser.max());
+ });
+ this.registerRangeMapper(new TypeToken>() {
+ }, parser -> {
+ if (!parser.hasMin() && !parser.hasMax()) {
+ return null;
+ }
+ return Range.of(parser.min(), parser.max());
+ });
+ this.registerRangeMapper(new TypeToken>() {
+ }, parser -> {
+ if (!parser.hasMin() && !parser.hasMax()) {
+ return null;
+ }
+ return Range.of(parser.min(), parser.max());
+ });
+ }
+
+ /**
+ * Registers the given range {@code mapper}.
+ *
+ * @param type produced by parser
+ * @param
parser type
+ * @param parserClass parser class
+ * @param mapper range mapper
+ */
+ public > void registerRangeMapper(
+ final @NonNull TypeToken
parserClass,
+ final @NonNull RangeMapper mapper
+ ) {
+ this.rangeMappers.put(GenericTypeReflector.erase(parserClass.getType()), mapper);
+ }
+
+ @Override
+ public @NonNull DiscordCommand create(final @NonNull CommandNode node) {
+ final CommandComponent component = node.component();
+ final List> options = new ArrayList<>();
+
+ CommandNode currentNode = node;
+ while (currentNode != null) {
+ boolean subCommand = false;
+ for (final CommandNode child : currentNode.children()) {
+ final DiscordOption childOption = this.createOption(child);
+ subCommand = subCommand || childOption instanceof DiscordOption.SubCommand;
+ options.add(childOption);
+ }
+
+ // If we encountered a subcommand or a subcommand group, then we let the subcommand deal with the
+ // options for us. Otherwise, we keep iterating until we've constructed the tree.
+ if (subCommand) {
+ break;
+ }
+
+ if (currentNode.isLeaf()) {
+ currentNode = null;
+ } else {
+ currentNode = currentNode.children().get(0);
+ }
+ }
+
+ return DiscordCommand.builder()
+ .name(component.name())
+ .description(node.component().description().textDescription())
+ .addAllOptions(options)
+ .build();
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private @NonNull DiscordOption createOption(final @NonNull CommandNode node) {
+ final CommandComponent component = node.component();
+
+ if (component.type() == CommandComponent.ComponentType.LITERAL) {
+ // We need to determine whether to flatten the children into a sub-command
+ // or whether to recursively extract the arguments.
+ List> children = new ArrayList<>(node.children()
+ .stream()
+ .map(this::createOption)
+ .collect(Collectors.toList()));
+
+ // If there's only one child and the child isn't a sub-command, then we recursively find the children.
+ if (children.size() == 1 && children.get(0) instanceof DiscordOption.Variable) {
+ children.clear();
+
+ CommandNode child = node.children().get(0);
+ while (child != null) {
+ children.add(this.createOption(child));
+
+ if (child.isLeaf()) {
+ child = null;
+ } else {
+ child = child.children().get(0);
+ }
+ }
+ }
+
+ return ImmutableSubCommand.builder()
+ .name(component.name())
+ .description(component.description().textDescription())
+ .addAllOptions(children)
+ .build();
+ }
+
+ final DiscordOptionType optionType = this.optionRegistry.getOption(component.valueType());
+ final Collection choices = this.extractChoices(component.suggestionProvider());
+ final Range range = this.extractRange(component.parser());
+
+ final boolean autoComplete;
+ if (choices.isEmpty()) {
+ autoComplete = DiscordOptionType.AUTOCOMPLETE.contains(optionType);
+ } else {
+ autoComplete = false;
+ }
+
+ return ImmutableVariable.