Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-29 17:33:28 +08:00
parent cf7d5814d6
commit 10ba20b045
30 changed files with 358 additions and 264 deletions

View file

@ -5,27 +5,31 @@ import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Asset file loader
*
* @property assetReader asset reader
* @property moshi moshi
*
* @see <a href = "https://www.notion.so/tangem/Assets-e045dd890413413faf34ce07ae47ff56">Documentation</a>
*
[REDACTED_AUTHOR]
*/
@Singleton
class AssetLoader @Inject constructor(
val assetReader: AssetReader,
@NetworkMoshi val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
) {
/** Load content [Content] of asset file [fileName] */
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun <reified Content> load(fileName: String): Content? {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
moshi.adapter<Content>().fromJson(json)
}
@ -43,8 +47,8 @@ class AssetLoader @Inject constructor(
/** Load list [V] values of asset file [fileName] */
suspend inline fun <reified V> loadList(fileName: String): List<V> {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
@ -65,8 +69,8 @@ class AssetLoader @Inject constructor(
/** Load map [String] keys and [V] values of asset file [fileName] */
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)

View file

@ -1,27 +1,23 @@
package com.tangem.datasource.asset.reader
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import android.content.res.AssetManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStream
import javax.inject.Inject
/**
* Implementation of asset file reader
*
* @property context application context
* @property assetManager asset manager
* @property dispatchers dispatchers
*/
internal class AndroidAssetReader @Inject constructor(
@ApplicationContext private val context: Context,
internal class AndroidAssetReader(
private val assetManager: AssetManager,
private val dispatchers: CoroutineDispatcherProvider,
) : AssetReader {
override fun readJson(fileName: String): String {
return openFile("$fileName.json")
.bufferedReader()
override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) {
assetManager.open(fullFileName).bufferedReader()
.use(BufferedReader::readText)
}
override fun openFile(file: String): InputStream {
return context.assets.open(file)
}
}

View file

@ -1,17 +1,18 @@
package com.tangem.datasource.asset.reader
import java.io.InputStream
/**
* Asset file reader
*
* @see <a href = "https://www.notion.so/tangem/Assets-e045dd890413413faf34ce07ae47ff56">Documentation</a>
*
[REDACTED_AUTHOR]
*/
interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
/** Open a file [file] from asset as InputStream */
fun openFile(file: String): InputStream
/**
* Read content of file from assets
*
* @param fullFileName name of file with extension. Example: file.json
*/
suspend fun read(fullFileName: String): String
}

View file

@ -10,7 +10,7 @@ interface ConfigManager {
val config: Config
fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null)
suspend fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null)
fun turnOff(name: String)

View file

@ -19,7 +19,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
private var defaultConfig = Config()
override fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)?) {
override suspend fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)?) {
configLoader.load { configModel ->
setupFeature(configModel.features)
setupConfigValues(configModel.configValues)

View file

@ -1,39 +1,25 @@
package com.tangem.datasource.config
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AssetReader instead")
class FeaturesLocalLoader(
private val assetReader: AssetReader,
private val moshi: Moshi,
private val assetLoader: AssetLoader,
buildEnvironment: String,
) : Loader<ConfigModel> {
private val featuresName = "features_$buildEnvironment"
private val configValuesName = "tangem-app-config/config_$buildEnvironment"
override fun load(onComplete: (ConfigModel) -> Unit) {
val config = try {
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = assetReader.readJson(featuresName)
val jsonConfigValues = assetReader.readJson(configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {
Timber.e(ex)
ConfigModel.empty()
}
onComplete(config)
override suspend fun load(onComplete: (ConfigModel) -> Unit) {
ConfigModel(
features = assetLoader.load<FeatureModel>(fileName = featuresName),
configValues = assetLoader.load<ConfigValueModel>(fileName = configValuesName),
).also(onComplete)
}
}

View file

@ -4,5 +4,5 @@ package com.tangem.datasource.config
[REDACTED_AUTHOR]
*/
interface Loader<T> {
fun load(onComplete: (T) -> Unit)
suspend fun load(onComplete: (T) -> Unit)
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AssetModule {
@Binds
@Singleton
fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader
}

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AssetReaderModule {
@Singleton
@Provides
fun providesAsserReader(
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): AssetReader {
return AndroidAssetReader(context.assets, dispatchers)
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import dagger.Module
@ -16,12 +14,11 @@ import javax.inject.Singleton
*/
@Module
@InstallIn(SingletonComponent::class)
object TestnetTokensStorageModule {
internal object TestnetTokensStorageModule {
@OptIn(ExperimentalStdlibApi::class)
@Provides
@Singleton
fun providesTestnetTokensStorage(assetReader: AssetReader, @SdkMoshi moshi: Moshi): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetReader = assetReader, adapter = moshi.adapter())
fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetLoader)
}
}

View file

@ -1,29 +1,21 @@
package com.tangem.datasource.local.testnet
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
/**
* Default implementation for storing testnet tokens data
*
* @property assetReader file reader from assets
* @property adapter adapter for parsing testnet tokens config
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class DefaultTestnetTokensStorage(
private val assetReader: AssetReader,
private val adapter: JsonAdapter<TestnetTokensConfig>,
private val assetLoader: AssetLoader,
) : TestnetTokensStorage {
@Deprecated(message = "Use AssetReader instead")
override fun getConfig(): TestnetTokensConfig {
return requireNotNull(
value = adapter.fromJson(
assetReader.readJson(fileName = LOCAL_CONFIG_PATH),
),
)
override suspend fun getConfig(): TestnetTokensConfig {
return requireNotNull(assetLoader.load<TestnetTokensConfig>(fileName = LOCAL_CONFIG_PATH))
}
private companion object {

View file

@ -10,5 +10,5 @@ import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
interface TestnetTokensStorage {
/** Get a testnet tokens data */
fun getConfig(): TestnetTokensConfig
suspend fun getConfig(): TestnetTokensConfig
}

View file

@ -0,0 +1,119 @@
package com.tangem.datasource.asset.loader
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.asset.reader.AssetReader
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalStdlibApi::class)
class AssetLoaderTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
@Test
fun load() = runTest {
everyReadingJson() returns assetObjectJson
val jsonAdapter = mockk<JsonAdapter<Asset>>()
every { moshi.adapter<Asset>() } returns jsonAdapter
every { jsonAdapter.fromJson(assetObjectJson) } returns assetObject
val actual = assetLoader.load<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetObjectJson)
}
Truth.assertThat(actual).isEqualTo(assetObject)
}
@Test
fun loadList() = runTest {
everyReadingJson() returns assetListJson
val jsonAdapter = mockk<JsonAdapter<List<Asset>>>()
val types = Types.newParameterizedType(List::class.java, Asset::class.java)
every { moshi.adapter<List<Asset>>(types) } returns jsonAdapter
every { jsonAdapter.fromJson(assetListJson) } returns assetList
val actual = assetLoader.loadList<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetListJson)
}
Truth.assertThat(actual).isEqualTo(assetList)
}
@Test
fun loadMap() = runTest {
everyReadingJson() returns assetMapJson
val jsonAdapter = mockk<JsonAdapter<Map<String, Asset>>>()
val types = Types.newParameterizedType(Map::class.java, String::class.java, Asset::class.java)
every { moshi.adapter<Map<String, Asset>>(types) } returns jsonAdapter
every { jsonAdapter.fromJson(assetMapJson) } returns assetMap
val actual = assetLoader.loadMap<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetMapJson)
}
Truth.assertThat(actual).isEqualTo(assetMap)
}
private fun everyReadingJson() = coEvery { assetReader.read("$JSON_FILE_NAME.json") }
private companion object {
const val JSON_FILE_NAME = "config"
val assetObject = Asset(
contractAddress = "0x0000000000000000000000000000000000000000",
network = "Network",
exchangeAvailable = true,
)
val assetList = listOf(assetObject, assetObject)
val assetMap = mapOf("key1" to assetObject, "key2" to assetObject)
val assetObjectJson = """
{
"contractAddress": "${assetObject.contractAddress}",
"network": "${assetObject.network}",
"exchangeAvailable": ${assetObject.exchangeAvailable}
}
""".trimIndent()
val assetListJson = """
$assetObjectJson,
$assetObjectJson
""".trimIndent()
val assetMapJson = """
{
"key1": $assetObjectJson,
"key2": $assetObjectJson
}
""".trimIndent()
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.google.common.truth.Truth
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.IOException
/**
[REDACTED_AUTHOR]
*/
internal class AndroidAssetReaderTest {
private val assetManager = mockk<AssetManager>()
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
@Test
fun read_content() = runTest {
every { assetManager.open(FILE_NAME) } returns json.byteInputStream()
val actual = assetReader.read(fullFileName = FILE_NAME)
Truth.assertThat(actual).isEqualTo(json)
}
@Test
fun read_error() = runTest {
val exception = IOException("Error")
every { assetManager.open(FILE_NAME) } throws exception
runCatching { assetReader.read(fullFileName = FILE_NAME) }
.onSuccess { throw IllegalStateException("Error should be thrown") }
.onFailure {
Truth.assertThat(it).isEqualTo(exception)
}
}
private companion object {
const val FILE_NAME = "file.json"
val json = """
{
"key": "value"
}
""".trimIndent()
}
}