-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
General feedback on the API design #49
Comments
Hi, this is an awesome feedback, wow! As I am trying to make everything as fast as possible - there are some issues that you have mentioned. I guess I will fix them all after I will finish the parser. I would like to mention that some of our remarks are related to coding conventions and it will follow https://github.com/cqfn/diKTat/blob/master/info/guide/diktat-coding-convention.md convention after diktat will be integrated here |
The only problem is with okio - I would really like people to have a simple method for reading Toml from file (as Toml is mostly used as a config), so I decided to use this one. As I tested - it is the only stable library for reading files in Kotlin right now :) |
That's understandable, I figured that it was probably best to leave some feedback this early on in the project before it's gone stable, because changing large things could be very cumbersome or even impossible when a library becomes more stable.
The conventions defined by diktat are very sensible, and especially the naming conventions they define seem to be largely following what I expressed/tried to explain regarding the
I personally really like okio, and with kotlinx-io essentially being postponed indefinitely for now, it's probably the best choice for an io library. Personally, I have no issue with an io library being a dependency, and having an easier way to read a toml file is a nice convenience, especially seeing as unlike formats like json, toml is made first and foremost to be a configuration language, so most of the time one would most likely be reading toml from a file. The reason I brought it up as a concern is because I know there are a lot of people who dislike extra dependency without an extremely solid reasoning (aka; this is required or this thing won't work), hence why a lot of libraries proudly boost about having "zero external dependencies". So I thought it'd be apt to at least comment about it, as I know there may be users why will raise issues regarding it later down the line. |
✅ I have added diktat and detekt to the project and fixed all style-related issues. And I am still in progress with other notes you have mentioned 😅 |
Sounds good! Just to clarify, I only meant one should use |
@Olivki yes, I renamed that class :) |
Hey @akuleshov7, nice work! I checked some parts of the code and have a suggestion to improve the public API. I agree with @Olivki that top-level interface Toml {
fun deserialize(value: String): TomlObject
companion object DefaultInstance : Toml by TomlImplementation() {
operator fun invoke(
ignoreUnknownNames: Boolean
): Toml = TomlImplementation(
ignoreUnknownNames = ignoreUnknownNames
)
}
}
internal class TomlImplementation(
val ignoreUnknownNames: Boolean = false
) : Toml {
override fun deserialize(value: String) = TODO()
}
// It's then possible to call both Toml.* functions directly for
// using default options or constructing custom Toml instances
// through "smart constructors" (= invoke operator fun)
fun main() {
Toml.deserialize("Hello")
Toml(ignoreUnknownNames = true).deserialize("Hello")
} |
@edrd-f @Olivki I can make the code look more close to kotlinx, what do you think about such API:
@ExperimentalSerializationApi
open class Toml(
private val config: KtomlConf = KtomlConf(),
override val serializersModule: SerializersModule = EmptySerializersModule
) : StringFormat {
// parser is created once, to reduce the number of created classes for each toml
val tomlParser = TomlParser(config)
/**
* The default instance of [Toml] with the default configuration.
* see [KtomlConf] for the list of the default options
*
* ThreadLocal annotation is used for caching
*/
@ThreadLocal
public companion object Default : Toml(KtomlConf())
// ================== basic overrides ===============
/**
* simple deserializer of a string in a toml format (separated by newlines)
*
* @param string - request-string in toml format with '\n' or '\r\n' separation
* @return deserialized object of type T
*/
override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
val parsedToml = tomlParser.parseString(string)
return TomlDecoder.decode(deserializer, parsedToml, config)
}
override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
TODO("Not yet implemented")
}
// ================== custom decoding methods ===============
/**
* partial deserializer of a string in a toml format (separated by newlines).
* Will deserialize only the part presented under the tomlTableName table.
* If such table is missing in he input - will throw an exception
*
* (!) Useful when you would like to deserialize only ONE table
* and you do not want to reproduce whole object structure in the code
*
* @param deserializer deserialization strategy
* @param toml request-string in toml format with '\n' or '\r\n' separation
* @param tomlTableName fully qualified name of the toml table (it should be the full name - a.b.c.d)
* @return deserialized object of type T
*/
fun <T> partiallyDecodeFromString(
deserializer: DeserializationStrategy<T>,
toml: String,
tomlTableName: String
): T {
val fakeFileNode = generateFakeTomlStructureForPartialParsing(toml, tomlTableName, TomlParser::parseString)
return TomlDecoder.decode(deserializer, fakeFileNode, config)
}
} You would be able to make the following calls:
(this is an extention method from kotlinx) THIS ONE
|
@akuleshov7 Looks great! |
I am also adding explicitApi() as a flag to the compiler. So we will need, for example, explicitly put |
Regarding the |
Hello, this isn't regarding one specific issue per se, but rather some general feedback regarding the design of the current API. If any of this comes off as aggressive/mean sounding, I apologize, my intention is solely for constructive criticism.
Most of my opinions will be based on the API design of officially supported format libraries developed by JetBrains themselves, which can be found here, and the Kotlin coding conventions provided by JetBrains, which can be found here. I'm not sure how much stuff you wanna change, but I figured it would be best to provide feedback while the library is still in early development, as a lot of these changes would break backwards compatibility.
There's a decent chunk of stuff that I want to provide feedback on, so I'm sorry if things read like a jumbled mess. I will try to section off the feedback to their own "sections" as best as I can.
If any of the suggestions here are something you like, I can make a pull requests with the fixes if desired. I would rather just explain my reasoning and thoughts before just making a pull requests with all fixes.
The
Ktoml
classThe class name
First point to address here is the name, if we look at naming rules, it states that
Names of classes and objects start with an uppercase letter and use camel case
, and with camel case, each new word should be capitalized, and for acronyms, each letter representing the word should normally be treated as a new word. meaning that if we follow these rules, the appropriate name for the class should beKToml
rather thanKtoml
as theK
stands for Kotlin, andtoml
should be treated as one word.(By following the above rules it should technically be
KTOML
, but if we look at the officially supported formats like json, and other classes developed by JetBrains, they seem to follow the rules of Dart wherein an acronym that's 3 or more characters long should be treated as a word, so instead ofURL
it would beUrl
.)However, if we look at essentially all other libraries, even those outside of the officially supported formats, like yamlkt and avro4k, they just use the format name as the class name, meaning that rather than
Ktoml
it would beToml
.Personally I think the nicest looking option is to just follow the official libraries and name the class
Toml
, as there is no real point in denoting that it's specifically for Kotlin as far as I can see.The general design of the config
I will be basing the following suggestion on the json library.
If we look at how the json library handles configuration, we can see that it's using a sealed class hierarchy to achieve this, which can be roughly laid out like this:
Json
class is the parent, you can not create new instances of directly, it has the implementations for the format its extending already defined.companion object Default
of theJson
class is the default implementation, which uses the default settings for serialization/deserialization.JsonImpl
class which allows custom settings to be set, this is internal and never exposed to the end user.JsonBuilder
class which allows the user to customize the settings, in conjunction with the top-levelJson
function this provides a nice Kotlin DSL for creating aJson
format with custom settings.Json
function acts as the constructor of theJson
class, allowing the user to create instances with a custom configuration easily.The benefit of this structure is that if I just want to use the default settings for the format, I can just write
Json.encodeToString
, orJson.Default.encodeToString
if I want to be more explicit. And when I want to change the settings I can just goJson { // stuff }
. It also allows the user to easily copy the settings from any already createdJson
instance, while keeping the settings of the instance immutable.If we apply the same design layout to ktoml it would roughly look like this:
The
deserialize
andserialize
top-level functionsThis is partly down to personal preference, but the absence of anything similar from most libraries should also be a tell-tale sign.
I do not think having these top-level functions actually add anything of value, I can see that the thought behind being that it might be easier to just call the top-level function rather than having to create a new
Ktoml
instance and call the relevant function. However, I can only see that this would bring readability issues and ambiguity going down the line.Here are some of the issues I can see would pop up from these functions:
deserializeToml
would be better, but it still feels like a code smell due to the other reasons defined below.deserialize
would actually deserialize into.serializer()
. function like what is done in these functions is way slower than explicitly passing in a serializer, as it requires reflection rather than just a direct function call. So encouraging the use of that function by making these functions so easily accessible is not good design imo.The dependency on
okio
I personally think dragging in a whole dependency just for inbuilt support for reading from a file is rather excessive, and I know a lot of other people also would like the dependency graph of the libraries they use to be as minimal as possible.
The inbuilt functions for reading from a file aren't that much of a time-saver either:
Ktoml.decodeFromFile(Thing.serializer(), "/foo/bar.toml")
vsKtoml.decodeFromString(Thing.serializer(), Path("/foo/bar.toml").readText())
(The above example is of course if you're on the JVM, but it's still relevant due to the argument below.)
There's also the fact that
okio
is not the only mulitplatform kotlin library that supports files, and while kotlinx.io is currently postponed, it will still be developed at one point, and there will certainly be more multiplatform file libraries developed. If this library then forces a dependency onokio
this could be annoying for users who would rather use another library.Therefore I think it would be better to not have explicit support for a specific file library, and rather just leave that up to the user. (Just quickly reading text from a file is more verbose in
okio
than in the Java path api with Kotlin extensions, but regardless, I don't think the minimal amount of boilerplate saved is worth explicitly forcing this library onto the user.)These suggestions are mainly only for the public facing API, as I haven't looked too deeply into the more internal API.
I hope no offense was taken from this, this is only meant as constructive criticism for a library I'm looking forward to use once it gets more stable.
The text was updated successfully, but these errors were encountered: