diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0a42ba7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ktlint_function_naming_ignore_when_annotated_with = Composable, Test, Preview \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0c28e78 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ChochaNaresh diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..18a6ebb --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing to formz + +We welcome pull requests! Please follow these _guidelines_: + +- Create issues for bugs and features. +- Fork and create feature branches (e.g., `feature/pick-*`). +- Follow existing code style and naming conventions. +- Run tests with `./gradlew test` before submitting a PR. +- Add documentation for any new public APIs. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b90eaab --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: ChochaNaresh diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..bacdf0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,29 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: [bug] +assignees: [] + +body: + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: The app crashes when... + validations: + required: true + + - type: input + attributes: + label: Library Version + placeholder: e.g. 1.0.0-beta.1 + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + placeholder: | + 1. Import library + 2. Call formz + 3. App crashes diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..93f3451 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a Question + url: https://github.com/ChochaNaresh/Formz/discussions + about: Please ask and answer questions in GitHub Discussions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..e739ea9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,17 @@ +name: Feature Request +description: Suggest a new feature or improvement +title: "[Feature]: " +labels: [enhancement] +assignees: [] + +body: + - type: textarea + attributes: + label: Feature Description + description: What feature would you like to see? + validations: + required: true + + - type: textarea + attributes: + label: Use case or motivation diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6e9baf4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## ✨ What's Changed + +Describe the changes you made in this PR. + +## ✅ Checklist + +- [ ] I've tested this on a device/emulator +- [ ] I've added tests where applicable +- [ ] I've updated documentation (if needed) +- [ ] This PR follows the [CONTRIBUTING.md](./CONTRIBUTING.md) guidelines diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f9f5052 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: Android CI/CD + +on: + push: + branches: [main, master] + tags: + - '[0-9]*' # matches 1.0.0, 0.1.0-alpha etc. + pull_request: + branches: [main, master] + +permissions: + contents: write # Required to create GitHub Releases + +jobs: + check: + name: Code Style & Formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Run checks + run: ./gradlew formz:spotlessCheck detekt + + - name: Upload Detekt HTML report + uses: actions/upload-artifact@v4 + with: + name: detekt-html-report + path: | + **/build/reports/detekt/detekt.html + build: + name: Build & Test + needs: check + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Run unit tests + run: ./gradlew formz:testDebugUnitTest + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: '**/build/test-results/testDebugUnitTest/' + + - name: Build library + run: ./gradlew formz:build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + **/build/outputs/aar/*.aar + **/build/libs/*.jar diff --git a/build.gradle.kts b/build.gradle.kts index 9963956..e7c69b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,60 @@ +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessPlugin +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.arturbosch.detekt) apply false + alias(libs.plugins.spotless) apply false +} + +val detektVersion = libs.versions.detekt.get() +subprojects { + // formatting code for all subprojects + apply() + configure { + kotlin { + target("src/**/*.kt") + targetExclude("build/**/*.kt") + ktlint() + endWithNewline() + } + kotlinGradle { + target("*.kts") + ktlint() + } + } + // code analysis for all subprojects + apply() + configure { + toolVersion = detektVersion + config.from("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = true + } + tasks.withType().configureEach { + reports { + xml.required.set(false) + html.required.set(true) + txt.required.set(false) + } + } + + afterEvaluate { + tasks.withType { + finalizedBy("spotlessApply") + } + + tasks.withType { + finalizedBy("detekt") + } + + } } \ No newline at end of file diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..901e007 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,787 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [ ] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: true + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [ 'Preview' ] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [ ] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + functionPattern: '[a-z][a-zA-Z0-9]*' + ignoreAnnotated: [ 'Composable' ] + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + SpreadOperator: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [ ] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [ ] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: [ '**/*.kts' ] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [ ] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [ ] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [ ] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [ ] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + ignoreAnnotated: [ 'Preview' ] + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/formz/build.gradle.kts b/formz/build.gradle.kts index fbb5121..ad77bfc 100644 --- a/formz/build.gradle.kts +++ b/formz/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.kotlin.android) @@ -6,10 +8,16 @@ plugins { android { namespace = "com.nareshchocha.formz" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -23,11 +31,13 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } } publishing { singleVariant("release") { @@ -38,6 +48,8 @@ android { } dependencies { + + testImplementation(libs.junit) } publishing { @@ -55,4 +67,4 @@ publishing { } } } -} \ No newline at end of file +} diff --git a/formz/src/main/java/com/nareshchocha/formz/Formz.kt b/formz/src/main/java/com/nareshchocha/formz/Formz.kt index 31342b9..cf5ae79 100644 --- a/formz/src/main/java/com/nareshchocha/formz/Formz.kt +++ b/formz/src/main/java/com/nareshchocha/formz/Formz.kt @@ -7,7 +7,10 @@ import java.util.Objects */ sealed class ValidationResult { data object Success : ValidationResult() - data class Failure(val error: E) : ValidationResult() + + data class Failure( + val error: E + ) : ValidationResult() } /** @@ -16,8 +19,10 @@ sealed class ValidationResult { * @param T The type of the input's value. * @param E The type of the validation error. */ -abstract class FormzInput(val value: T, val isPure: Boolean) { - +abstract class FormzInput( + val value: T, + val isPure: Boolean +) { // Cache the validation result. Since 'value' is immutable, this is safe. private val validationResult: ValidationResult by lazy { validator(value) } @@ -39,24 +44,20 @@ abstract class FormzInput(val value: T, val isPure: Boolean) { /** * Returns a [ValidationResult.Failure] if validation fails, or `null` if it succeeds. */ - fun error(): ValidationResult.Failure? { - return when (validationResult) { + fun error(): ValidationResult.Failure? = + when (validationResult) { is ValidationResult.Failure -> validationResult as ValidationResult.Failure ValidationResult.Success -> null } - } /** * Returns the error to display. * * If the input is still pure (unmodified), no error is displayed. */ - fun displayError(): E? = - if (isPure) null else error()?.error + fun displayError(): E? = if (isPure) null else error()?.error - override fun hashCode(): Int { - return Objects.hash(value, isPure) - } + override fun hashCode(): Int = Objects.hash(value, isPure) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -68,9 +69,7 @@ abstract class FormzInput(val value: T, val isPure: Boolean) { return true } - override fun toString(): String { - return "FormzInput(value=$value, isPure=$isPure, isValid=${isValid()}, error=${error()})" - } + override fun toString(): String = "FormzInput(value=$value, isPure=$isPure, isValid=${isValid()}, error=${error()})" } /** @@ -80,16 +79,12 @@ object Formz { /** * Returns `true` if all provided inputs are valid. */ - fun validate(inputs: List>): Boolean { - return inputs.all { it.isValid() } - } + fun validate(inputs: List>): Boolean = inputs.all { it.isValid() } /** * Returns `true` if all provided inputs are still pure. */ - fun isPure(inputs: List>): Boolean { - return inputs.all { it.isPure } - } + fun isPure(inputs: List>): Boolean = inputs.all { it.isPure } } /** diff --git a/formz/src/test/java/com/nareshchocha/formz/FormzInputTest.kt b/formz/src/test/java/com/nareshchocha/formz/FormzInputTest.kt new file mode 100644 index 0000000..c26ccd5 --- /dev/null +++ b/formz/src/test/java/com/nareshchocha/formz/FormzInputTest.kt @@ -0,0 +1,276 @@ +package com.nareshchocha.formz + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FormzInputTest { + private class NonEmptyStringInput( + value: String, + isPure: Boolean = true + ) : FormzInput(value, isPure) { + override fun validator(value: String): ValidationResult = + if (value.isNotEmpty()) ValidationResult.Success else ValidationResult.Failure("Empty") + } + + @Test + fun isValid_returnsTrue_whenInputIsPure() { + val input = NonEmptyStringInput("", isPure = true) + assertTrue(input.isValid()) + } + + @Test + fun isValid_returnsTrue_whenInputIsValidAndDirty() { + val input = NonEmptyStringInput("abc", isPure = false) + assertTrue(input.isValid()) + } + + @Test + fun isValid_returnsFalse_whenInputIsInvalidAndDirty() { + val input = NonEmptyStringInput("", isPure = false) + assertFalse(input.isValid()) + } + + @Test + fun error_returnsNull_whenInputIsValid() { + val input = NonEmptyStringInput("abc", isPure = false) + assertNull(input.error()) + } + + @Test + fun error_returnsFailure_whenInputIsInvalid() { + val input = NonEmptyStringInput("", isPure = false) + val error = input.error() + assertNotNull(error) + assertEquals("Empty", error?.error) + } + + @Test + fun displayError_returnsNull_whenInputIsPure() { + val input = NonEmptyStringInput("", isPure = true) + assertNull(input.displayError()) + } + + @Test + fun displayError_returnsError_whenInputIsInvalidAndDirty() { + val input = NonEmptyStringInput("", isPure = false) + assertEquals("Empty", input.displayError()) + } + + @Test + fun displayError_returnsNull_whenInputIsValidAndDirty() { + val input = NonEmptyStringInput("abc", isPure = false) + assertNull(input.displayError()) + } + + @Test + fun equals_and_hashCode_workCorrectly() { + val input1 = NonEmptyStringInput("abc", isPure = false) + val input2 = NonEmptyStringInput("abc", isPure = false) + val input3 = NonEmptyStringInput("def", isPure = false) + assertEquals(input1, input2) + assertEquals(input1.hashCode(), input2.hashCode()) + assertNotEquals(input1, input3) + } + + @Test + fun isValid_returnsFalse_whenInputIsNull() { + val input = + object : FormzInput(null, isPure = false) { + override fun validator(value: String?): ValidationResult = + if (value.isNullOrEmpty()) ValidationResult.Failure("Null or Empty") else ValidationResult.Success + } + assertFalse(input.isValid()) + } + + @Test + fun error_returnsFailure_whenInputIsNull() { + val input = + object : FormzInput(null, isPure = false) { + override fun validator(value: String?): ValidationResult = + if (value.isNullOrEmpty()) ValidationResult.Failure("Null or Empty") else ValidationResult.Success + } + val error = input.error() + assertNotNull(error) + assertEquals("Null or Empty", error?.error) + } + + @Test + fun displayError_returnsError_whenInputIsNullAndDirty() { + val input = + object : FormzInput(null, isPure = false) { + override fun validator(value: String?): ValidationResult = + if (value.isNullOrEmpty()) ValidationResult.Failure("Null or Empty") else ValidationResult.Success + } + assertEquals("Null or Empty", input.displayError()) + } + + @Test + fun equals_and_hashCode_workCorrectly_withNullValues() { + val input1 = + object : FormzInput(null, isPure = false) { + override fun validator(value: String?): ValidationResult = + if (value.isNullOrEmpty()) ValidationResult.Failure("Null or Empty") else ValidationResult.Success + } + val input2 = + object : FormzInput(null, isPure = false) { + override fun validator(value: String?): ValidationResult = + if (value.isNullOrEmpty()) ValidationResult.Failure("Null or Empty") else ValidationResult.Success + } + assertEquals(input1, input2) + assertEquals(input1.hashCode(), input2.hashCode()) + } + + @Test + fun isValid_returnsTrue_whenInputIsWhitespaceAndConsideredValid() { + val input = + object : FormzInput(" ", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.isBlank()) ValidationResult.Success else ValidationResult.Failure("Not Blank") + } + assertTrue(input.isValid()) + } + + @Test + fun error_returnsFailure_whenInputIsWhitespaceAndInvalid() { + val input = + object : FormzInput(" ", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.isBlank()) ValidationResult.Failure("Blank Input") else ValidationResult.Success + } + val error = input.error() + assertNotNull(error) + assertEquals("Blank Input", error?.error) + } + + @Test + fun displayError_returnsError_whenInputIsWhitespaceAndDirty() { + val input = + object : FormzInput(" ", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.isBlank()) ValidationResult.Failure("Blank Input") else ValidationResult.Success + } + assertEquals("Blank Input", input.displayError()) + } + + @Test + fun isValid_returnsFalse_whenInputIsEmptyString() { + val input = + object : FormzInput("", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.isEmpty()) ValidationResult.Failure("Empty String") else ValidationResult.Success + } + assertFalse(input.isValid()) + } + + @Test + fun equals_and_hashCode_workCorrectly_withWhitespaceValues() { + val input1 = + object : FormzInput(" ", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.isBlank()) ValidationResult.Success else ValidationResult.Failure("Not Blank") + } + val input2 = + object : FormzInput(" ", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.isBlank()) ValidationResult.Success else ValidationResult.Failure("Not Blank") + } + assertEquals(input1, input2) + assertEquals(input1.hashCode(), input2.hashCode()) + } + + @Test + fun isValid_returnsFalse_whenInputExceedsMaxLength() { + val input = + object : FormzInput("a".repeat(101), isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.length > 100) ValidationResult.Failure("Exceeds Max Length") else ValidationResult.Success + } + assertFalse(input.isValid()) + } + + @Test + fun error_returnsFailure_whenInputExceedsMaxLength() { + val input = + object : FormzInput("a".repeat(101), isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.length > 100) ValidationResult.Failure("Exceeds Max Length") else ValidationResult.Success + } + val error = input.error() + assertNotNull(error) + assertEquals("Exceeds Max Length", error?.error) + } + + @Test + fun isValid_returnsFalse_whenInputIsNegativeNumber() { + val input = + object : FormzInput(-1, isPure = false) { + override fun validator(value: Int): ValidationResult = + if (value < 0) ValidationResult.Failure("Negative Number") else ValidationResult.Success + } + assertFalse(input.isValid()) + } + + @Test + fun error_returnsFailure_whenInputIsNegativeNumber() { + val input = + object : FormzInput(-1, isPure = false) { + override fun validator(value: Int): ValidationResult = + if (value < 0) ValidationResult.Failure("Negative Number") else ValidationResult.Success + } + val error = input.error() + assertNotNull(error) + assertEquals("Negative Number", error?.error) + } + + @Test + fun isValid_returnsTrue_whenInputIsBoundaryValue() { + val input = + object : FormzInput(0, isPure = false) { + override fun validator(value: Int): ValidationResult = + if (value >= 0) ValidationResult.Success else ValidationResult.Failure("Invalid Boundary") + } + assertTrue(input.isValid()) + } + + @Test + fun isValid_returnsFalse_whenInputIsSpecialCharacters() { + val input = + object : FormzInput("@#$%", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.any { it.isLetterOrDigit() }) { + ValidationResult.Success + } else { + ValidationResult.Failure( + "Special Characters" + ) + } + } + assertFalse(input.isValid()) + } + + @Test + fun error_returnsFailure_whenInputIsSpecialCharacters() { + val input = + object : FormzInput("@#$%", isPure = false) { + override fun validator(value: String): ValidationResult = + if (value.any { it.isLetterOrDigit() }) { + ValidationResult.Success + } else { + ValidationResult.Failure( + "Special Characters" + ) + } + } + val error = input.error() + assertNotNull(error) + assertEquals("Special Characters", error?.error) + } +} diff --git a/formz/src/test/java/com/nareshchocha/formz/FormzInterfaceTest.kt b/formz/src/test/java/com/nareshchocha/formz/FormzInterfaceTest.kt new file mode 100644 index 0000000..3baa107 --- /dev/null +++ b/formz/src/test/java/com/nareshchocha/formz/FormzInterfaceTest.kt @@ -0,0 +1,107 @@ +package com.nareshchocha.formz + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FormzInterfaceTest { + private class DummyInput( + value: String, + isPure: Boolean + ) : FormzInput(value, isPure) { + override fun validator(value: String): ValidationResult = + if (value == "ok") ValidationResult.Success else ValidationResult.Failure("fail") + } + + private class DummyForm( + val input1: DummyInput, + val input2: DummyInput + ) : FormzInterface { + override val inputs = listOf(input1, input2) + } + + @Test + fun isValid_returnsTrue_whenAllInputsAreValid() { + val form = DummyForm(DummyInput("ok", false), DummyInput("ok", false)) + assertTrue(form.isValid) + assertFalse(form.isNotValid) + } + + @Test + fun isValid_returnsFalse_whenAnyInputIsInvalid() { + val form = DummyForm(DummyInput("ok", false), DummyInput("bad", false)) + assertFalse(form.isValid) + assertTrue(form.isNotValid) + } + + @Test + fun isPure_returnsTrue_whenAllInputsArePure() { + val form = DummyForm(DummyInput("ok", true), DummyInput("ok", true)) + assertTrue(form.isPure) + assertFalse(form.isDirty) + } + + @Test + fun isPure_returnsFalse_whenAnyInputIsDirty() { + val form = DummyForm(DummyInput("ok", true), DummyInput("ok", false)) + assertFalse(form.isPure) + assertTrue(form.isDirty) + } + + @Test + fun isValid_returnsTrue_whenInputsListIsEmpty() { + val form = + object : FormzInterface { + override val inputs = emptyList>() + } + assertTrue(form.isValid) + assertFalse(form.isNotValid) + } + + @Test + fun isPure_returnsTrue_whenInputsListIsEmpty() { + val form = + object : FormzInterface { + override val inputs = emptyList>() + } + assertTrue(form.isPure) + assertFalse(form.isDirty) + } + + @Test + fun isValid_and_isPure_workCorrectly_withMixedInputs() { + val validPure = + object : FormzInput(1, true) { + override fun validator(value: Int) = ValidationResult.Success + } + val invalidDirty = + object : FormzInput(0, false) { + override fun validator(value: Int) = ValidationResult.Failure("fail") + } + val form = + object : FormzInterface { + override val inputs = listOf(validPure, invalidDirty) + } + assertFalse(form.isValid) + assertTrue(form.isNotValid) + assertFalse(form.isPure) + assertTrue(form.isDirty) + } + + @Test + fun isValid_and_isPure_workCorrectly_withDuplicateInputs() { + val input = + object : FormzInput(1, true) { + override fun validator(value: Int) = ValidationResult.Success + } + val form = + object : FormzInterface { + override val inputs = listOf(input, input) + } + assertTrue(form.isValid) + assertTrue(form.isPure) + } +} diff --git a/formz/src/test/java/com/nareshchocha/formz/FormzTest.kt b/formz/src/test/java/com/nareshchocha/formz/FormzTest.kt new file mode 100644 index 0000000..be9a036 --- /dev/null +++ b/formz/src/test/java/com/nareshchocha/formz/FormzTest.kt @@ -0,0 +1,125 @@ +package com.nareshchocha.formz + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FormzTest { + private class AlwaysValidInput : FormzInput(0, false) { + override fun validator(value: Int) = ValidationResult.Success + } + + private class AlwaysInvalidInput : FormzInput(0, false) { + override fun validator(value: Int) = ValidationResult.Failure("Invalid") + } + + @Test + fun validate_returnsTrue_whenAllInputsAreValid() { + val inputs = listOf(AlwaysValidInput(), AlwaysValidInput()) + assertTrue(Formz.validate(inputs)) + } + + @Test + fun validate_returnsFalse_whenAnyInputIsInvalid() { + val inputs = listOf(AlwaysValidInput(), AlwaysInvalidInput()) + assertFalse(Formz.validate(inputs)) + } + + @Test + fun isPure_returnsTrue_whenAllInputsArePure() { + val pureInputs = + listOf( + object : FormzInput(0, true) { + override fun validator(value: Int) = ValidationResult.Success + }, + object : FormzInput(1, true) { + override fun validator(value: Int) = ValidationResult.Success + } + ) + assertTrue(Formz.isPure(pureInputs)) + } + + @Test + fun isPure_returnsFalse_whenAnyInputIsDirty() { + val pureInput = + object : FormzInput(0, true) { + override fun validator(value: Int) = ValidationResult.Success + } + val dirtyInput = + object : FormzInput(1, false) { + override fun validator(value: Int) = ValidationResult.Success + } + assertFalse(Formz.isPure(listOf(pureInput, dirtyInput))) + } + + @Test + fun validate_returnsTrue_whenInputsListIsEmpty() { + val inputs = emptyList>() + assertTrue(Formz.validate(inputs)) + } + + @Test + fun validate_returnsFalse_whenAllInputsAreInvalid() { + val inputs = + listOf( + object : FormzInput(0, false) { + override fun validator(value: Int) = ValidationResult.Failure("Invalid") + }, + object : FormzInput(1, false) { + override fun validator(value: Int) = ValidationResult.Failure("Invalid") + } + ) + assertFalse(Formz.validate(inputs)) + } + + @Test + fun isPure_returnsTrue_whenInputsListIsEmpty() { + val inputs = emptyList>() + assertTrue(Formz.isPure(inputs)) + } + + @Test + fun isPure_returnsFalse_whenAllInputsAreDirty() { + val inputs = + listOf( + object : FormzInput(0, false) { + override fun validator(value: Int) = ValidationResult.Success + }, + object : FormzInput(1, false) { + override fun validator(value: Int) = ValidationResult.Success + } + ) + assertFalse(Formz.isPure(inputs)) + } + + @Test + fun validate_handlesMixedValidAndInvalidInputs() { + val inputs = + listOf( + object : FormzInput(0, false) { + override fun validator(value: Int) = ValidationResult.Success + }, + object : FormzInput(1, false) { + override fun validator(value: Int) = ValidationResult.Failure("Invalid") + } + ) + assertFalse(Formz.validate(inputs)) + } + + @Test + fun isPure_handlesMixedPureAndDirtyInputs() { + val inputs = + listOf( + object : FormzInput(0, true) { + override fun validator(value: Int) = ValidationResult.Success + }, + object : FormzInput(1, false) { + override fun validator(value: Int) = ValidationResult.Success + } + ) + assertFalse(Formz.isPure(inputs)) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dafcabe..0ce522e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,18 @@ [versions] -agp = "8.8.2" -kotlin = "2.1.0" -compileSdk = "35" -targetSdk = "35" +agp = "8.11.0" +kotlin = "2.2.0" +detekt = "1.23.8" +compileSdk = "36" +targetSdk = "36" minSdk = "21" -jdkVersion = "VERSION_17" -coreKtx = "1.15.0" +coreKtx = "1.16.0" +core-splashscreen="1.0.1" -lifecycleRuntimeKtx = "2.8.7" +lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" -composeBom = "2025.02.00" -material3v = "1.3.1" +composeBom = "2025.06.01" +material3v = "1.3.2" timber = "5.0.1" @@ -22,6 +23,7 @@ espressoCore = "3.6.1" [libraries] # core +core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "core-splashscreen" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -50,4 +52,7 @@ android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +# code style review +arturbosch-detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +spotless = { id = "com.diffplug.spotless", version = "7.0.4" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 337d736..a1797fa 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Aug 09 10:49:58 IST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 34c8566..fb04e63 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) @@ -6,12 +8,21 @@ plugins { android { namespace = "com.nareshchocha.sample" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { applicationId = "com.nareshchocha.sample" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" @@ -31,11 +42,13 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.valueOf(libs.versions.jdkVersion.get()) - targetCompatibility = JavaVersion.valueOf(libs.versions.jdkVersion.get()) + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = JavaVersion.valueOf(libs.versions.jdkVersion.get()).toString() + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } } buildFeatures { compose = true @@ -43,11 +56,10 @@ android { } dependencies { - + implementation(libs.core.splashscreen) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.ui) @@ -67,4 +79,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/sample/src/androidTest/java/com/nareshchocha/sample/ExampleInstrumentedTest.kt b/sample/src/androidTest/java/com/nareshchocha/sample/ExampleInstrumentedTest.kt index 0b1d6a0..d45b787 100644 --- a/sample/src/androidTest/java/com/nareshchocha/sample/ExampleInstrumentedTest.kt +++ b/sample/src/androidTest/java/com/nareshchocha/sample/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.nareshchocha.sample -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.nareshchocha.formz", appContext.packageName) } -} \ No newline at end of file +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 1954b3e..3258e30 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -10,13 +10,11 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Formz" + android:theme="@style/Theme.Formz.Splash" tools:targetApi="31"> + android:exported="true"> diff --git a/sample/src/main/ic_launcher-playstore.png b/sample/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..71d7091 Binary files /dev/null and b/sample/src/main/ic_launcher-playstore.png differ diff --git a/sample/src/main/java/com/nareshchocha/sample/MainActivity.kt b/sample/src/main/java/com/nareshchocha/sample/MainActivity.kt index ee450f4..0d4ffd8 100644 --- a/sample/src/main/java/com/nareshchocha/sample/MainActivity.kt +++ b/sample/src/main/java/com/nareshchocha/sample/MainActivity.kt @@ -2,7 +2,9 @@ package com.nareshchocha.sample import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -14,11 +16,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,6 +31,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -34,6 +41,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.nareshchocha.sample.baseComponents.AppOutlinedTextField import com.nareshchocha.sample.ui.theme.SampleTheme import com.nareshchocha.sample.velidate.BooleanInput @@ -45,22 +53,56 @@ import com.nareshchocha.sample.velidate.getErrorMessage class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - //enableEdgeToEdge() + installSplashScreen() + enableEdgeToEdge( + statusBarStyle = + SystemBarStyle.dark( + Color.Transparent.toArgb() + ), + navigationBarStyle = + SystemBarStyle.light( + Color.Transparent.toArgb(), + Color.Transparent.toArgb() + ) + ) setContent { SampleTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - AllComponents( - modifier = Modifier - .fillMaxSize() - .padding(14.dp) - .padding(innerPadding) - ) - } + RootUI() } } } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RootUI() { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text( + text = "Formz Sample", + style = MaterialTheme.typography.headlineMedium.copy(color = Color.White) + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) + } + ) { innerPadding -> + AllComponents( + modifier = + Modifier + .fillMaxSize() + .padding(14.dp) + .padding(innerPadding) + ) + } +} + @Composable fun AllComponents(modifier: Modifier = Modifier) { var testInputTextField by remember { mutableStateOf(NonEmptyInput()) } @@ -77,21 +119,26 @@ fun AllComponents(modifier: Modifier = Modifier) { Text(text = "Test") }, isError = !testInputTextField.isValid(), - errorMassage = testInputTextField.displayError() - ?.getErrorMessage("Test"), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - keyboardType = KeyboardType.Text, imeAction = ImeAction.Next - ), - - modifier = Modifier.fillMaxWidth(), + errorMassage = + testInputTextField + .displayError() + ?.getErrorMessage("Test"), + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + modifier = Modifier.fillMaxWidth() ) PasswordTextField( value = passwordInputTextField.value, isError = !passwordInputTextField.isValid(), - errorMassage = passwordInputTextField.displayError() - ?.getErrorMessage("Password"), + errorMassage = + passwordInputTextField + .displayError() + ?.getErrorMessage("Password"), onValueChange = { println("PasswordTextField= $it") passwordInputTextField = passwordInputTextField.copy(it) @@ -116,11 +163,9 @@ fun AllComponents(modifier: Modifier = Modifier) { ) } } - } } - @Composable private fun PasswordTextField( value: String, @@ -134,20 +179,24 @@ private fun PasswordTextField( var showPassword by remember { mutableStateOf(value = false) } AppOutlinedTextField( - value = value, onValueChange = onValueChange, + value = value, + onValueChange = onValueChange, label = { Text(text = "password") }, isError = isError, errorMassage = errorMassage, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, imeAction = ImeAction.Done - ), - visualTransformation = if (showPassword) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + visualTransformation = + if (showPassword) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, trailingIcon = { if (showPassword) { IconButton(onClick = { showPassword = false }) { @@ -158,7 +207,8 @@ private fun PasswordTextField( } } else { IconButton( - onClick = { showPassword = true }) { + onClick = { showPassword = true } + ) { Icon( imageVector = Icons.Filled.VisibilityOff, contentDescription = "hide_password" @@ -167,18 +217,18 @@ private fun PasswordTextField( } }, modifier = modifier.fillMaxWidth(), - keyboardActions = KeyboardActions(onDone = { - focusManager.clearFocus() - onClick() - }) + keyboardActions = + KeyboardActions(onDone = { + focusManager.clearFocus() + onClick() + }) ) } - @Preview(showBackground = true) @Composable fun GreetingPreview() { SampleTheme { AllComponents() } -} \ No newline at end of file +} diff --git a/sample/src/main/java/com/nareshchocha/sample/baseComponents/AppOutlinedTextField.kt b/sample/src/main/java/com/nareshchocha/sample/baseComponents/AppOutlinedTextField.kt index 0a58849..d884659 100644 --- a/sample/src/main/java/com/nareshchocha/sample/baseComponents/AppOutlinedTextField.kt +++ b/sample/src/main/java/com/nareshchocha/sample/baseComponents/AppOutlinedTextField.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation - @Composable fun AppOutlinedTextField( value: String, @@ -38,7 +37,7 @@ fun AppOutlinedTextField( keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, - minLines: Int = 1, + minLines: Int = 1 ) { OutlinedTextField( value = value, @@ -50,30 +49,36 @@ fun AppOutlinedTextField( label = label, placeholder = placeholder, leadingIcon = leadingIcon, - trailingIcon = trailingIcon - ?: if (isError) { - { - Icon(Icons.Filled.Error, "error", tint = MaterialTheme.colorScheme.error) - } - } else null, + trailingIcon = + trailingIcon + ?: if (isError) { + { + Icon(Icons.Filled.Error, "error", tint = MaterialTheme.colorScheme.error) + } + } else { + null + }, prefix = prefix, suffix = suffix, - supportingText = supportingText - ?: if (isError && errorMassage!=null) { - { - Text( - modifier = Modifier.fillMaxWidth(), - text = errorMassage ?: "", - color = MaterialTheme.colorScheme.error - ) - } - } else null, + supportingText = + supportingText + ?: if (isError && errorMassage != null) { + { + Text( + modifier = Modifier.fillMaxWidth(), + text = errorMassage ?: "", + color = MaterialTheme.colorScheme.error + ) + } + } else { + null + }, isError = isError, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = singleLine, maxLines = maxLines, - minLines = minLines, + minLines = minLines ) -} \ No newline at end of file +} diff --git a/sample/src/main/java/com/nareshchocha/sample/ui/theme/Color.kt b/sample/src/main/java/com/nareshchocha/sample/ui/theme/Color.kt index b20e463..7462546 100644 --- a/sample/src/main/java/com/nareshchocha/sample/ui/theme/Color.kt +++ b/sample/src/main/java/com/nareshchocha/sample/ui/theme/Color.kt @@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/sample/src/main/java/com/nareshchocha/sample/ui/theme/Theme.kt b/sample/src/main/java/com/nareshchocha/sample/ui/theme/Theme.kt index b6364b6..c38d5e2 100644 --- a/sample/src/main/java/com/nareshchocha/sample/ui/theme/Theme.kt +++ b/sample/src/main/java/com/nareshchocha/sample/ui/theme/Theme.kt @@ -10,17 +10,18 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 +private val DarkColorScheme = + darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 + ) +private val LightColorScheme = + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 /* Other default colors to override background = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE), @@ -29,8 +30,8 @@ private val LightColorScheme = lightColorScheme( onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), - */ -) + */ + ) @Composable fun SampleTheme( @@ -39,19 +40,20 @@ fun SampleTheme( dynamicColor: Boolean = true, content: @Composable () -> Unit ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/sample/src/main/java/com/nareshchocha/sample/ui/theme/Type.kt b/sample/src/main/java/com/nareshchocha/sample/ui/theme/Type.kt index 450cb5c..97adab0 100644 --- a/sample/src/main/java/com/nareshchocha/sample/ui/theme/Type.kt +++ b/sample/src/main/java/com/nareshchocha/sample/ui/theme/Type.kt @@ -7,28 +7,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/sample/src/main/java/com/nareshchocha/sample/velidate/AnyNonEmptyInput.kt b/sample/src/main/java/com/nareshchocha/sample/velidate/AnyNonEmptyInput.kt index b74d4fc..380547a 100644 --- a/sample/src/main/java/com/nareshchocha/sample/velidate/AnyNonEmptyInput.kt +++ b/sample/src/main/java/com/nareshchocha/sample/velidate/AnyNonEmptyInput.kt @@ -7,11 +7,11 @@ class AnyNonEmptyInput( value: T, isPure: Boolean = true ) : FormzInput(value, isPure) { - override fun validator(value: T): ValidationResult { - return if (value == null) ValidationResult.Failure(ValidationError.INVALID) else ValidationResult.Success - } + override fun validator(value: T): ValidationResult = + if (value == null) ValidationResult.Failure(ValidationError.INVALID) else ValidationResult.Success - fun copy(value: T, isPure: Boolean = false): AnyNonEmptyInput { - return AnyNonEmptyInput(value, isPure = isPure) - } + fun copy( + value: T, + isPure: Boolean = false + ): AnyNonEmptyInput = AnyNonEmptyInput(value, isPure = isPure) } diff --git a/sample/src/main/java/com/nareshchocha/sample/velidate/BooleanInput.kt b/sample/src/main/java/com/nareshchocha/sample/velidate/BooleanInput.kt index c500d22..d10f6d4 100644 --- a/sample/src/main/java/com/nareshchocha/sample/velidate/BooleanInput.kt +++ b/sample/src/main/java/com/nareshchocha/sample/velidate/BooleanInput.kt @@ -7,11 +7,11 @@ class BooleanInput( value: Boolean = false, isPure: Boolean = true ) : FormzInput(value, isPure) { - override fun validator(value: Boolean): ValidationResult { - return if (value) ValidationResult.Success else ValidationResult.Failure(ValidationError.NOT_SELECTED) - } + override fun validator(value: Boolean): ValidationResult = + if (value) ValidationResult.Success else ValidationResult.Failure(ValidationError.NOT_SELECTED) - fun copy(value: Boolean, isPure: Boolean = false): BooleanInput { - return BooleanInput(value, isPure = isPure) - } + fun copy( + value: Boolean, + isPure: Boolean = false + ): BooleanInput = BooleanInput(value, isPure = isPure) } diff --git a/sample/src/main/java/com/nareshchocha/sample/velidate/NonEmptyInput.kt b/sample/src/main/java/com/nareshchocha/sample/velidate/NonEmptyInput.kt index c59f02f..b80c38c 100644 --- a/sample/src/main/java/com/nareshchocha/sample/velidate/NonEmptyInput.kt +++ b/sample/src/main/java/com/nareshchocha/sample/velidate/NonEmptyInput.kt @@ -7,11 +7,11 @@ class NonEmptyInput( value: String = "", isPure: Boolean = true ) : FormzInput(value, isPure) { - override fun validator(value: String): ValidationResult { - return if (value.isNotBlank()) ValidationResult.Success else ValidationResult.Failure(ValidationError.EMPTY) - } + override fun validator(value: String): ValidationResult = + if (value.isNotBlank()) ValidationResult.Success else ValidationResult.Failure(ValidationError.EMPTY) - fun copy(value: String, isPure: Boolean = false): NonEmptyInput { - return NonEmptyInput(value, isPure = isPure) - } + fun copy( + value: String, + isPure: Boolean = false + ): NonEmptyInput = NonEmptyInput(value, isPure = isPure) } diff --git a/sample/src/main/java/com/nareshchocha/sample/velidate/RegexInput.kt b/sample/src/main/java/com/nareshchocha/sample/velidate/RegexInput.kt index 9ceac93..9dfafd4 100644 --- a/sample/src/main/java/com/nareshchocha/sample/velidate/RegexInput.kt +++ b/sample/src/main/java/com/nareshchocha/sample/velidate/RegexInput.kt @@ -3,35 +3,28 @@ package com.nareshchocha.sample.velidate import com.nareshchocha.formz.FormzInput import com.nareshchocha.formz.ValidationResult - class RegexInput( private val regex: String, private val skipEmpty: Boolean = false, value: String = "", isPure: Boolean = true ) : FormzInput(value, isPure) { - override fun validator(value: String): ValidationResult { - return if (value.isEmpty()) { + override fun validator(value: String): ValidationResult = + if (value.isEmpty()) { if (skipEmpty) ValidationResult.Success else ValidationResult.Failure(ValidationError.EMPTY) } else if (!Regex(regex).matches(value)) { ValidationResult.Failure(ValidationError.INVALID) } else { ValidationResult.Success } - } - fun copy(value: String, isPure: Boolean = false): RegexInput { - return RegexInput(regex, skipEmpty, value, isPure = isPure) - } + fun copy( + value: String, + isPure: Boolean = false + ): RegexInput = RegexInput(regex, skipEmpty, value, isPure = isPure) } - object RegularExpressions { - const val USERNAME = "^[A-Za-z0-9_.-]{3,15}$"; - const val EMAIL = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+"; - const val MOBILE_NUMBER = "^\\ d{10}$"; const val PASSWORD = - "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$"; - const val URL = - "((https?:www\\.)|(https?:\\/\\/)|(www\\.))[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9]{1,6}(\\/[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)?"; -} \ No newline at end of file + "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$" +} diff --git a/sample/src/main/java/com/nareshchocha/sample/velidate/validation_error.kt b/sample/src/main/java/com/nareshchocha/sample/velidate/ValidationError.kt similarity index 82% rename from sample/src/main/java/com/nareshchocha/sample/velidate/validation_error.kt rename to sample/src/main/java/com/nareshchocha/sample/velidate/ValidationError.kt index 2d8ee8d..1332674 100644 --- a/sample/src/main/java/com/nareshchocha/sample/velidate/validation_error.kt +++ b/sample/src/main/java/com/nareshchocha/sample/velidate/ValidationError.kt @@ -3,12 +3,10 @@ package com.nareshchocha.sample.velidate enum class ValidationError { EMPTY, INVALID, - NOT_SELECTED, + NOT_SELECTED } -fun List.combineErrors(): ValidationError? { - return this.filterNotNull().firstOrNull() -} +fun List.combineErrors(): ValidationError? = this.filterNotNull().firstOrNull() fun ValidationError.getErrorMessage( field: String, @@ -23,4 +21,4 @@ fun ValidationError.getErrorMessage( ValidationError.NOT_SELECTED -> "You must select $field" else -> throw IllegalArgumentException("Unsupported ValidationError type") } -} \ No newline at end of file +} diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/sample/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..036d09b 100644 --- a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..036d09b 100644 --- a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.webp b/sample/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..39f5784 100644 Binary files a/sample/src/main/res/mipmap-hdpi/ic_launcher.webp and b/sample/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3bf6515 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..634e582 100644 Binary files a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.webp b/sample/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..04c1f0e 100644 Binary files a/sample/src/main/res/mipmap-mdpi/ic_launcher.webp and b/sample/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/sample/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b00ebd4 Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..45e20e6 100644 Binary files a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp b/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..65f0ddd 100644 Binary files a/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..e31f881 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..a812565 100644 Binary files a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..019847d 100644 Binary files a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..acbbc60 Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..55ed73e 100644 Binary files a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..819a939 100644 Binary files a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..1fe6c66 Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..d060b34 100644 Binary files a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/sample/src/main/res/values-night/themes.xml b/sample/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..6cf451d --- /dev/null +++ b/sample/src/main/res/values-night/themes.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/ic_launcher_background.xml b/sample/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/sample/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/sample/src/main/res/values/themes.xml b/sample/src/main/res/values/themes.xml index 5d7ac4b..bf81393 100644 --- a/sample/src/main/res/values/themes.xml +++ b/sample/src/main/res/values/themes.xml @@ -1,5 +1,23 @@ - - + - + + + + \ No newline at end of file diff --git a/sample/src/test/java/com/nareshchocha/sample/ExampleUnitTest.kt b/sample/src/test/java/com/nareshchocha/sample/ExampleUnitTest.kt index 0348bd6..a9278e7 100644 --- a/sample/src/test/java/com/nareshchocha/sample/ExampleUnitTest.kt +++ b/sample/src/test/java/com/nareshchocha/sample/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.nareshchocha.sample +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +}