From 4e42c761734258dcc1a469acc3d5876ae605e9b6 Mon Sep 17 00:00:00 2001 From: nenoeldeeb <69523673+Nenoeldeeb@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:48:58 +0300 Subject: [PATCH 1/4] Add .editorconfig file to simplify contributing process. --- .editorconfig | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fefe1f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,138 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_size = 4 +indent_style = space +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +guidelines = 120 + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# JS files +[*.{js,ts,scss,html}] +indent_size = 2 + +[*.{ts}] +quote_type = single + +[*.{scss,yml,csproj}] +indent_size = 2 + +[*.sln] +indent_style = tab + +# Dotnet code style settings: +[*.{cs,vb}] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Prefix private members with underscore +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + +# Async methods should have "Async" suffix +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.severity = suggestion + +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async + +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.word_separator = + +# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items. +dotnet_diagnostic.CS0618.severity = suggestion + +# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items. +dotnet_diagnostic.CS0612.severity = suggestion + +# Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005 +dotnet_diagnostic.IDE0005.severity = warning + +# CSharp code style settings: +[*.cs] +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a expression-body +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = true:none +csharp_style_expression_bodied_operators = true:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Namespace settings +csharp_style_namespace_declarations = file_scoped:warning + +# Switch expression +dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value +dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value From 70711704e404d271ebd3ab2f5cf0123ed67e04c4 Mon Sep 17 00:00:00 2001 From: nenoeldeeb <69523673+Nenoeldeeb@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:34:42 +0300 Subject: [PATCH 2/4] Implement the setData method.In this commit, I will implement the setData method. It will be the opposite of the getData method. It will be implemented in BaseState as set each field data & in FormState as set all fields in the form. --- .../java/com/dsc/form_builder/BaseState.kt | 16 +++++++++++++++ .../java/com/dsc/form_builder/FormState.kt | 20 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt b/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt index c7888b3..d116fc6 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt @@ -69,6 +69,22 @@ abstract class BaseState( return if (transform == null) value else transform.transform(value) } + /** + *Change the field value to the specified data. + * + * @param data the data to set the value to. + * @throws IllegalArgumentException If the data type doesn't match the type of the value property. + * does not match the type of the initial value. + */ + open fun setData(data: Any) { + requireNotNull(initial) + require(data::class.simpleName == initial!!::class.simpleName) { + "Input type must be ${initial!!::class.simpleName}" + } + @Suppress("UNCHECKED_CAST") + value = data as T + } + /** * This function resets all form field values to their initial states. */ diff --git a/form-builder/src/main/java/com/dsc/form_builder/FormState.kt b/form-builder/src/main/java/com/dsc/form_builder/FormState.kt index f2cfe5b..dd27de6 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/FormState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/FormState.kt @@ -2,6 +2,7 @@ package com.dsc.form_builder import kotlin.reflect.KClass import kotlin.reflect.KParameter +import kotlin.reflect.full.memberProperties /** * This class represents the state of the whole form, i.e, the whole collection of fields. It is used to manage all of the states in terms of accessing data and validations. @@ -36,7 +37,7 @@ open class FormState>(val fields: List) { if (!it.isOptional) { checkNotNull(value) { """ - A non-null value (${it.name}) in your class doesn't have a matching field in the form data. + A non-null value (${it.name}) in your class doesn't have a matching field in the form data. This will throw a NullPointerException when creating $dataClass. To solve this, you can: 1. Make the value nullable in your data class 2. Provide a default value for the parameter @@ -48,6 +49,23 @@ open class FormState>(val fields: List) { return constructor.callBy(args) } + /** + * Set the form fields value to data argument properties. + * + * @param data The data source for the form values, which must be a data class. + * */ + fun setData(data: Any) { + require(data::class.isData) { + "Data must be a data class" + } + data::class.memberProperties.forEach { property -> + if (property.visibility.toString() != "PRIVATE") { + val value = property.call(data) + fields.find { it.name == property.name }?.setData(value!!) + } + } + } + /** * This function is used to reset data in all the form fields to their initial values. */ From 375fa5912ec45a65e8ad0991b208f09d443a8b74 Mon Sep 17 00:00:00 2001 From: nenoeldeeb <69523673+Nenoeldeeb@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:38:25 +0300 Subject: [PATCH 3/4] Add tests for setData & getData methods. I will add tests for setData method to be sure about the correctness of setData implementation. Also, I will add test for getData method. --- .../com/dsc/form_builder/FormStateTest.kt | 61 +++++++++++++++++-- .../com/dsc/form_builder/SelectStateTest.kt | 21 +++++++ .../com/dsc/form_builder/SwitchStateTest.kt | 36 +++++++---- .../dsc/form_builder/TextFieldStateTest.kt | 7 +++ 4 files changed, 106 insertions(+), 19 deletions(-) diff --git a/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt index 9a82c49..d25f28d 100755 --- a/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt @@ -5,9 +5,18 @@ import org.junit.jupiter.api.Test internal class FormStateTest { + internal data class FormTestDataClass( + val email: String, + val hobbies: List, + val gender: String, + val age: String, + val active: Boolean, + private val date: Int = 0 + ) + @Nested inner class DescribingFormState { - + private val formState = FormState( listOf( TextFieldState(name = "email"), @@ -20,13 +29,13 @@ internal class FormStateTest { SwitchState(name = "active") ) ) - + private val emailState = formState.getState("email") private val hobbyState = formState.getState("hobbies") private val genderState = formState.getState("gender") private val ageState = formState.getState("age") private val statusState = formState.getState("active") - + @Test fun `state should be reset to initial values`() { // Given a form state with values changed @@ -35,10 +44,10 @@ internal class FormStateTest { genderState.change("male") ageState.change("56") statusState.toggle() - + // When the form.reset is requested formState.reset() - + // Then all values are reset to the original state assert(emailState.value == "" && !emailState.hasError) assert(hobbyState.value == mutableListOf() && !hobbyState.hasError) @@ -46,5 +55,45 @@ internal class FormStateTest { assert(ageState.value == "34" && !ageState.hasError) assert(!statusState.value && !statusState.hasError) } + + @Test + fun `getData gets the form's data correctly`() { + ageState.change("16") + emailState.change("testget@form.com") + hobbyState.select("Running") + hobbyState.select("Reading") + genderState.change("male") + statusState.toggle() + + val data = formState.getData(FormTestDataClass::class) + + assert(data.email == emailState.value) + assert(data.hobbies == hobbyState.value) + assert(data.gender == genderState.value) + assert(data.age == ageState.value) + } + + @Test + fun `setData should set the correct values`() { + val data = FormTestDataClass( + email = "buider@gmail.com", + hobbies = listOf( + "Running", + "Reading" + ), + gender = "male", + age = "56", + active = true, + date = 12 + ) + formState.setData(data) + + val data2 = formState.getData(FormTestDataClass::class) + assert(data2.email == data.email) + assert(data2.hobbies == data.hobbies) + assert(data2.gender == data.gender) + assert(data2.age == data.age) + assert(data2.active == data.active) + } } -} \ No newline at end of file +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt index 7382f25..7e3ece4 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt @@ -68,5 +68,26 @@ internal class SelectStateTest { val actual = classToTest.validateMax(max, "expected validation: $expected") assert(actual == expected) } + + @Test + fun `setData works correctly`() { + val value = mutableListOf( + "item 1", + "item 2" + ) + classToTest.setData(value) + assert(classToTest.value == value) + } + + @Test + fun `setData fails with incorrect data type`() { + val value = "item 1" + + assert( + runCatching { + classToTest.setData(value) + }.isFailure + ) + } } } diff --git a/form-builder/src/test/java/com/dsc/form_builder/SwitchStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/SwitchStateTest.kt index 6094300..4b16a58 100755 --- a/form-builder/src/test/java/com/dsc/form_builder/SwitchStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/SwitchStateTest.kt @@ -4,50 +4,60 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test internal class SwitchStateTest { - + @Nested inner class DescribingStateChanges { - + private val classToTest: SwitchState = SwitchState(name = "test") - + @Test fun `errors should be hidden when toggled`() { // Simulate an existing validation error classToTest.hasError = true classToTest.errorMessage = "error message" - + classToTest.toggle() // When toggle is called - + assert(!classToTest.hasError) assert(classToTest.errorMessage.isEmpty()) } - + @Test fun `state should be updated when toggled`() { val initialState = classToTest.value classToTest.toggle() // When toggle is called - + assert(classToTest.value != initialState) - + classToTest.toggle() // Toggle again assert(classToTest.value == initialState) } } - + @Nested inner class DescribingValidation { - + private val classToTest: SwitchState = SwitchState(name = "test") - + @Test fun `Validators_Required works correctly`() { // When state is false (off) val firstValidation = classToTest.validateRequired("") assert(!firstValidation) - + classToTest.toggle() // Turn on the switch val secondValidation = classToTest.validateRequired("") assert(secondValidation) } + + @Test + fun `setData works correctly`() { + val value = true + classToTest.setData(value) + assert(classToTest.value == value) + + classToTest.setData(false) + assert(classToTest.value == false) + } } -} \ No newline at end of file +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt index c1f947b..b8ac3c7 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt @@ -134,5 +134,12 @@ internal class TextFieldStateTest { val actual = classToTest.validateDate("expected validation: $expected", pattern) assert(actual == expected) } + + @Test + fun `setData works correctly`() { + val value = "test" + classToTest.setData(value) + assert(classToTest.value == value) + } } } From e15ae98b2bedb1cf133aab40b3e92b50c4079456 Mon Sep 17 00:00:00 2001 From: nenoeldeeb <69523673+Nenoeldeeb@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:53:40 +0300 Subject: [PATCH 4/4] Fix a null values not checked in setData & add test for that. --- .../src/main/java/com/dsc/form_builder/FormState.kt | 5 +++-- .../src/test/java/com/dsc/form_builder/FormStateTest.kt | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/form-builder/src/main/java/com/dsc/form_builder/FormState.kt b/form-builder/src/main/java/com/dsc/form_builder/FormState.kt index dd27de6..cbd36d7 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/FormState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/FormState.kt @@ -60,8 +60,9 @@ open class FormState>(val fields: List) { } data::class.memberProperties.forEach { property -> if (property.visibility.toString() != "PRIVATE") { - val value = property.call(data) - fields.find { it.name == property.name }?.setData(value!!) + property.call(data)?.let { value -> + fields.find { it.name == property.name }?.setData(value) + } } } } diff --git a/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt index d25f28d..067b5c1 100755 --- a/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt @@ -11,7 +11,8 @@ internal class FormStateTest { val gender: String, val age: String, val active: Boolean, - private val date: Int = 0 + private val date: Int = 0, + val password: String? = null ) @Nested @@ -76,7 +77,7 @@ internal class FormStateTest { @Test fun `setData should set the correct values`() { val data = FormTestDataClass( - email = "buider@gmail.com", + email = "testset@form.com", hobbies = listOf( "Running", "Reading" @@ -84,7 +85,8 @@ internal class FormStateTest { gender = "male", age = "56", active = true, - date = 12 + date = 12, + password = "123" ) formState.setData(data)