Updated on 2026-08-14
This commit is contained in:
parent
cf7d5814d6
commit
10ba20b045
30 changed files with 358 additions and 264 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue