Match string to object property - kotlin

I have a set of policies that I want to match a request. If the policy exists, I want to match the request and see if the value matches.
The policies are List<Policies> -> (key: String, value: String) and the request can contain different keys.
Example:
The policies are a set of rules that the request should match.
class Policy {
val key: String,
val value: String
}
The request is a data class that contains different values (all optional), for example surname, firstName, address, ++++
data class Request (
id: Long = 12,
firstName: String = "test",
surname: String = "test",
address: String = "somewhere"
...// more fields
)
The list of policies can look like this (List):
List<Policy> => [
{
key: "surname",
value: "test"
},
{
key: "firstName",
value: "test"
}
]
I don't know how I can match the policies with the request. The Policy.key is a String and the Request can contain all different variations of properties.
How do I match the List of policies with my data class Request?

For your puroposes you need use reflection (you want to find field by name and get value), or change something in your model.
Solution with reflection can be like following:
data class Policy(
val key: String,
val value: String?
)
data class Request(
val id: Int,
val firstName: String? = null,
val surname: String? = null,
val address: String? = null
)
class PolicyException : Exception()
fun checkPolicies(request: Request, policies: List<Policy>) {
policies.forEach { policy ->
val member = request::class.members.find { member -> member.name == policy.key }
val requestMemberValue = member?.call(request)
if (requestMemberValue != policy.value) throw PolicyException()
}
}
fun main(args: Array<String>) {
println("Hello, reflection!")
checkPolicies(Request(id = 0, firstName = "Johnn"), listOf(Policy("firstName", "John")))
}
Also, I changed your policy model to handle nullable values (and still handling properly "null" as string).
But, with this solution you have to be very careful with changing model names. And remeber to do not obfuscate your models.
Also, quite better soltuion is adding Annotation which keeps policy name as annotation property (then problem with changing field name in app will disappear).

Related

Kotlin get all property value of data class

Is there a syntactic sugar in Kotlin to iterate on each field/property value of a data class?
Sample:
data class User(
var firstName: String = DEFAULT_VALUE_STRING,
var middleName: String = DEFAULT_VALUE_STRING,
var lastName: String = DEFAULT_VALUE_STRING
)
val user = User()
Then check if any of the property's value is empty, considering all of it is String data type with something like this
if (user.properties.any{ it.isBlank() }) {
// TODO ...
}
Probably the closest you'll get is checking all the values of all the generated componentX() functions (since they're only created for the constructor parameter properties, the "data" in a data class) but yeah that involves reflection.
If I were you, I'd create an interface with a properties property and make all your data classes implement that - something like this:
import kotlin.reflect.KProperty0
interface HasStringProperties {
val properties: List<KProperty0<String>>
}
data class User(
var firstName: String = "",
var middleName: String = "",
var lastName: String = ""
) : HasStringProperties {
override val properties = listOf(::firstName, ::middleName, ::lastName)
}
fun main() {
val user = User("Funny", "", "Name")
println(user.properties.any {it.get().isBlank()})
}
So no, it's not automatic - but specifying which properties you want to include is simple, and required if you're going to access it on a particular class, so there's an element of safety there.
Also, because you're explicitly specifying String properties, there's type safety included as well. Your example code is implicitly assuming all properties on your data classes will be Strings (or at least, they're a type with an isBlank() function) which isn't necessarily going to be true. You'd have to write type-checking into your reflection code - if you say "I don't need to, the classes will only have String parameters" then maybe that's true, until it isn't. And then the reflection code has to be written just because you want to add a single age field or whatever.
You don't actually have to use property references in Kotlin either, you could just grab the current values:
interface HasStringProperties {
val properties: List<String>
}
data class User(
var firstName: String = "",
var middleName: String = "",
var lastName: String = ""
) : HasStringProperties {
// getter function creating a new list of current values every time it's accessed
override val properties get() = listOf(firstName, middleName, lastName)
}
fun main() {
val user = User("Funny", "", "Name")
println(user.properties.any {it.isBlank()})
}
It depends whether you want to be able to reference the actual properties on the class itself, or delegate to a getter to fetch the current values.
And of course you could use generics if you want, list all the properties and use filterIsInstance<String> to pull all the strings. And you could put a function in the interface to handle a generic isEmpty check for different types. Put all the "check these properties aren't 'empty'" code in one place, so callers don't need to concern themselves with working that out and what it means for each property

Room Database - Compare values in the database to userinput for validation

I am creating an app where the user can log in or register for an account. I have created the register screen and it's successfully saving data into the database. However, I am now trying to integrate some validation. For example, the usernames would have to be unique and the email can't already exist.
I tried to write a custom query of-course to print out all the rows in the username column like this:
SELECT userName from cx_table
and I also tried to write a separate custom query of-course to print out all the rows in the email column like this:
SELECT email from cx_table
Then my approach was to take the user input and compare it to the values returned by that column, if it exists, print an error message. But when I run the app, I get the following error message
The columns returned by the query does not have the fields [id,firstName,lastName,password,address,city,postalcode,email,phone] in com.cxpro.data.Customer even though they are annotated as non-null or primitive. Columns returned by the query: [userName]
here is all my code for the Room Database:
Customer.kt
#Entity(tableName = "cx_table")
data class Customer(
#PrimaryKey(autoGenerate = true)
val id: Int,
val firstName: String,
val lastName: String,
val userName: String,
val password: String,
val address: String,
val city: String,
val postalcode: String,
val email: String,
val phone: String
)
CustomerDao.kt
#Dao
interface CustomerDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addCustomers(customer: Customer)
#Query("SELECT * FROM cx_table ORDER BY id ASC")
fun readAllData(): LiveData<List<Customer>>
#Query("SELECT userName FROM cx_table")
fun readUserName(): LiveData<List<Customer>>
}
CustomerDatabase.kt
#Database(entities = [Customer::class],version = 1, exportSchema = false)
abstract class CustomerDatabase: RoomDatabase() {
abstract fun customerDao(): CustomerDao
companion object{
#Volatile
private var INSTANCE: CustomerDatabase? = null
fun getDatabase(context: Context): CustomerDatabase{
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
CustomerDatabase::class.java,
"customer_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
CustomerRepository.kt
class CustomerRepository(private val customerDao: CustomerDao) {
val readAllData: LiveData<List<Customer>> = customerDao.readAllData()
val readUserName: LiveData<List<Customer>> = customerDao.readUserName()
suspend fun addCustomer(customer: Customer){
customerDao.addCustomers(customer)
}
}
CustomerViewModel.kt
class CustomerViewModel(application: Application): AndroidViewModel(application) {
val readAllData: LiveData<List<Customer>>
val readUserName: LiveData<List<Customer>>
private val repository: CustomerRepository
init {
val customerDao = CustomerDatabase.getDatabase(application).customerDao()
repository = CustomerRepository(customerDao)
readAllData = repository.readAllData
readUserName = repository.readUserName
}
fun addCustomer(customer: Customer){
viewModelScope.launch(Dispatchers.IO){
repository.addCustomer(customer)
}
}
}
How can I validate that the username and/or email doesn't already exist in the table?
Then my approach was to take the user input and compare it to the values returned by that column, if it exists, print an error message. But when I run the app, I get the following error message
This is because there are insufficient values to build a Customer object. As you just returning a single value per row you can use List<String> instead of List<Customer>. If multiple values then you need an object probably a POJO. and that the object field/variable names match the columns names
However, rather then having to loop through 2 lists you may wish to consider changing the Customer Entity to :-
#Entity(tableName = "cx_table",
indices = [
Index(value = ["userName"],unique = true),
Index(value = ["email"],unique = true)]
)
data class Customer(
#PrimaryKey(autoGenerate = true)
val id: Int,
val firstName: String,
val lastName: String,
val userName: String,
val password: String,
val address: String,
val city: String,
val postalcode: String,
val email: String,
val phone: String
)
and also changing the insert Dao to be :-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addCustomers(customer: Customer): Long //<<<<< ADDED Long
You can the check the return value if it is greater than 0 then the row was inserted, otherwise the row wasn't inserted and thus was invalid.
That is, as the indexes on username and email are UNIQUE then attempting to insert whilst duplicating either would result in a conflict, which is ignored. However, the row is not inserted and hence -1 being returned.
Yet another option could be to test the values e.g.
#Query("SELECT count(*) FROM customer WHERE userName=:userNameToCheck OR email=emailToCheck")
fun validateNewCustomer(userNameToCheck: String,emailToCheck): Int
If the result is 0 then OK to insert. You could split into two checks if you wanted to check them individually.
You could ascertain whether it is userName or email that is resulting in the invalid (non-zero) result using something like:-
#Query("SELECT ((SELECT count(*) FROM customer WHERE username=:userNameToCheck) + (SELECT count(*) * 1000 FROM customer WHERE email=:emailToCheck));")
fun validateNewCustomer(userNameToCheck: String,emailToCheck): Int
If the returned value is 0 then valid, if less than 1000 (if greater than 1 then duplicates exist) then the userName is invalid, if greater 1000 then the email is invalid, if greater than 1000 but not an exact multiple of 1000 then both are invalid.
1000 caters for up to 998 userName duplicates (if there is a UNIQUE index on userName then there should only be 1, similar for email)

How manage data classes with empty data in retrofit?

I want to send to the API only the data that I indicate in my data class.
data class UserRequestDTO(
val name: String = "",
val surname: String = ""
)
My empty class
val userDTO = UserRequestDTO()
MyService.getService().users()
My request
#POST("/vo/search")
fun users(
#Body userRequestDTO: UserRequestDTO
): Call<ResponseDTO>
But the following json is being sent:
{"name": "", "surname": ""}
How can I have a class in which I only send the data that I fill in? don't want any data to be sent, and if for example I fill in the name, I don't want the last name to be sent
Making them optional should result in not sending them:
data class UserRequestDTO(
val name: String? = null,
val surname: String? = null
)

Kotlin Reflection - Returns an instance with set properties values based on it's property name

I'm trying to create a function that returns any data class object setting it's property values with its property names (if all strings) without changing it's default values
I have an example on how it is:
Imagine this data class:
data class StudentProfile(
var fullName: String = "",
var mobilePhone: String = "",
var birthDate: String = "",
var email: String = ""
)
I want to keep this empty default values, but I wanted a generic function that should work for any class and returns (in this case) this:
return StudentProfile(
mobilePhone = "mobilePhone",
fullName = "fullName",
email = "email",
birthDate = "birthDate"
)
Is it possible?
This does sound like an X-Y problem (I can't imagine how it would be useful), but I thought it'd be fun to solve anyway.
I'm unclear about whether you want to replace default values or not (since you say you don't but your example does), so this example lets you choose.
Explanation: Make sure all of the constructor's parameters are Strings or optional (have defaults). Otherwise, this is impossible because non-String parameter values could not be specified. Then filter the parameters list to include only the ones we are setting to their own name, and associate them to their names to create a Map<KParameter, String> that we can pass to constructor.callBy.
fun <T: Any> produceWithPropertiesByOwnName(type: KClass<T>, overrideDefaults: Boolean): T {
val constructor = type.primaryConstructor!!
val parameters = constructor.parameters
if (!parameters.all { param -> param.type.classifier == String::class || param.isOptional }){
error("Class $type primary constructor has required non-String parameters.")
}
val valuesByParameter = parameters.filter { it.type.classifier == String::class && (!it.isOptional || overrideDefaults) }
.associateWith(KParameter::name)
return constructor.callBy(valuesByParameter)
}

Create instance of class kotlin

How can I create an instance of InfoA that contains also title. Do I need to modify the classes?
Can't specify the title.
Also, do I need to create setters for it? To not access with the _
val info = InfoA(_subtitle = "SUBTITLE", title = ...)
open class Info(
open val action: Action = Action(),
open val title: String? = ""
) {
fun hasAction(): Boolean = action.hasAction()
}
class InfoA(
private val _subtitle: String? = "",
private val _image: String? = "",
private val _backgroundImage: String? = "",
private val _backgroundColor: String? = null,
private val _foregroundColor: String? = null,
private val _borderColor: String? = null
) : Info() {
val subtitle: String
get() = _subtitle.orEmpty()
val image: String
get() = _image.orEmpty()
val backgroundImage: String
get() = _backgroundImage.orEmpty()
val backgroundColor: Int?
get() = if (_backgroundColor != null) convertRgbStringToColorInt(_backgroundColor) else null
val foregroundColor: Int?
get() = if (_foregroundColor != null) convertRgbStringToColorInt(_foregroundColor) else null
val borderColor: Int?
get() = if (_borderColor != null) convertRgbStringToColorInt(_borderColor) else null
}
As the code is written, title is a val, so it can't be changed from its initial value — which is empty string if (as in the case of InfoA) something calls its constructor without specifying another value.
If it were changed to be a var, then it could be changed later, e.g.:
val info = InfoA(_subtitle = "SUBTITLE").apply{ title = "..." }
Alternatively, if you want to keep it a val, then InfoA would need to be changed: the most obvious way would be to add a title parameter in its constructor, and pass that up to Info:
class InfoA(
title: String? = "",
// …other fields…
) : Info(title = title) {
Note that this way, InfoA can never use Info's default value for title, so you may need to duplicate that default in InfoA's constructor.
The need to duplicate superclass properties in a subclass constructor is awkward, but there's currently no good way around it.  (See e.g. this question.)  If there are many parameters, you might consider bundling them together into a single data class, which could then be passed easily up to the superclass constructor — but of course users of the class would need to specify that.  (Some people think that having more than a few parameters is a code smell, and that bundling them together can often improve the design.)