edu-format/src/com/jetbrains/edu/learning/json/mixins/LocalEduCourseMixins.kt (469 lines of code) (raw):
@file:JvmName("LocalEduCourseMixins")
@file:Suppress("unused")
package com.jetbrains.edu.learning.json.mixins
import com.fasterxml.jackson.annotation.JacksonInject
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyOrder
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.ObjectCodec
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.util.StdConverter
import com.jetbrains.edu.learning.courseFormat.*
import com.jetbrains.edu.learning.courseFormat.EduFormatNames.MARKETPLACE
import com.jetbrains.edu.learning.courseFormat.tasks.*
import com.jetbrains.edu.learning.courseFormat.EmtpyFileContentFactory
import com.jetbrains.edu.learning.courseFormat.FILE_CONTENTS_FACTORY_INJECTABLE_VALUE
import com.jetbrains.edu.learning.courseFormat.FileContentsFactory
import com.jetbrains.edu.learning.courseFormat.tasks.choice.ChoiceOption
import com.jetbrains.edu.learning.courseFormat.tasks.choice.ChoiceOptionStatus
import com.jetbrains.edu.learning.courseFormat.tasks.choice.ChoiceTask
import com.jetbrains.edu.learning.courseFormat.tasks.matching.MatchingTask
import com.jetbrains.edu.learning.courseFormat.tasks.matching.SortingTask
import com.jetbrains.edu.learning.json.encrypt.Encrypt
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.ADDITIONAL_FILES
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.AUTHORS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.CHOICE_OPTIONS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.COURSE_TYPE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.CUSTOM_NAME
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.DEPENDENCY
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.DESCRIPTION_FORMAT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.DESCRIPTION_TEXT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.DISABLED_FEATURES
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.ENVIRONMENT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.ENVIRONMENT_SETTINGS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.FEEDBACK_LINK
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.FILE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.FILES
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.FRAMEWORK_TYPE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.HIGHLIGHT_LEVEL
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.IS_BINARY
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.IS_EDITABLE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.IS_MULTIPLE_CHOICE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.IS_VISIBLE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.ITEMS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.ITEM_TYPE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.LANGUAGE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.LENGTH
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.LESSON
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.MAX_VERSION
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.MESSAGE_CORRECT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.MESSAGE_INCORRECT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.MIN_VERSION
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.NAME
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.OFFSET
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.CUSTOM_CONTENT_PATH
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PLACEHOLDER
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PLACEHOLDERS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PLACEHOLDER_TEXT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PLUGINS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PLUGIN_ID
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PLUGIN_NAME
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.POSSIBLE_ANSWER
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PROGRAMMING_LANGUAGE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PROGRAMMING_LANGUAGE_ID
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.PROGRAMMING_LANGUAGE_VERSION
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.QUIZ_HEADER
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.SECTION
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.SOLUTIONS_HIDDEN
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.SOLUTION_HIDDEN
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.STATUS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.SUMMARY
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TAGS
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TASK
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TASK_LIST
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TASK_TYPE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TEXT
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TITLE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.TYPE
import com.jetbrains.edu.learning.json.mixins.JsonMixinNames.VERSION
private val LOG = logger<LocalEduCourseMixin>()
@JsonPropertyOrder(
ENVIRONMENT, SUMMARY, TITLE, PROGRAMMING_LANGUAGE_ID, PROGRAMMING_LANGUAGE_VERSION, LANGUAGE,
COURSE_TYPE, SOLUTIONS_HIDDEN, PLUGINS, ITEMS, AUTHORS, TAGS, ADDITIONAL_FILES, CUSTOM_CONTENT_PATH, VERSION
)
abstract class LocalEduCourseMixin {
@JsonProperty(TITLE)
private lateinit var name: String
@JsonProperty(AUTHORS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonSerialize(contentConverter = UserInfoToString::class)
@JsonDeserialize(contentConverter = MarketplaceUserInfoFromString::class)
private var authors: MutableList<UserInfo> = ArrayList()
@JsonProperty(SUMMARY)
private lateinit var description: String
@Suppress("SetterBackingFieldAssignment")
private var programmingLanguage: String? = null
@JsonProperty(PROGRAMMING_LANGUAGE)
set(value) {
if (formatVersion >= JSON_FORMAT_VERSION_WITH_NEW_LANGUAGE_VERSION || value.isNullOrEmpty()) return
value.split(" ").apply {
languageId = first()
languageVersion = getOrNull(1)
}
}
@JsonProperty(PROGRAMMING_LANGUAGE_ID)
private lateinit var languageId: String
@JsonProperty(PROGRAMMING_LANGUAGE_VERSION)
private var languageVersion: String? = null
@JsonProperty(LANGUAGE)
private lateinit var languageCode: String
@JsonProperty(ENVIRONMENT)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
lateinit var environment: String
@JsonProperty(ENVIRONMENT_SETTINGS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
lateinit var environmentSettings: Map<String, String>
val itemType: String
@JsonProperty(COURSE_TYPE)
get() = throw NotImplementedInMixin()
@JsonProperty(ITEMS)
private lateinit var _items: List<StudyItem>
@JsonProperty(ADDITIONAL_FILES)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var additionalFiles: List<EduFile>
@JsonProperty(CUSTOM_CONTENT_PATH)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
lateinit var customContentPath: String
@JsonProperty(PLUGINS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var pluginDependencies: List<PluginInfo>
@JsonProperty(SOLUTIONS_HIDDEN)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private var solutionsHidden: Boolean = false
@JsonProperty(TAGS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var contentTags: List<String>
@JsonProperty(VERSION)
private var formatVersion = JSON_FORMAT_VERSION
@JsonProperty(DISABLED_FEATURES)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var disabledFeatures: List<String>
}
private class UserInfoToString : StdConverter<UserInfo, String?>() {
override fun convert(value: UserInfo?): String? = value?.getFullName()
}
private class MarketplaceUserInfoFromString : StdConverter<String?, JBAccountUserInfo?>() {
override fun convert(value: String?): JBAccountUserInfo? {
if (value == null) {
return null
}
return JBAccountUserInfo(value)
}
}
abstract class PluginInfoMixin : PluginInfo() {
@JsonProperty(PLUGIN_ID)
override var stringId: String = ""
@JsonProperty(PLUGIN_NAME)
@JsonInclude(JsonInclude.Include.NON_NULL)
override var displayName: String? = null
@JsonProperty(MIN_VERSION)
@JsonInclude(JsonInclude.Include.NON_NULL)
override var minVersion: String? = null
@JsonProperty(MAX_VERSION)
@JsonInclude(JsonInclude.Include.NON_NULL)
override var maxVersion: String? = null
}
@JsonPropertyOrder(TITLE, CUSTOM_NAME, TAGS, ITEMS, TYPE)
abstract class LocalSectionMixin {
@JsonProperty(TITLE)
private lateinit var name: String
@JsonProperty(CUSTOM_NAME)
@JsonInclude(JsonInclude.Include.NON_NULL)
private var customPresentableName: String? = null
@JsonProperty(ITEMS)
private lateinit var _items: List<StudyItem>
val itemType: String
@JsonProperty(TYPE)
get() = throw NotImplementedInMixin()
@JsonProperty(TAGS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var contentTags: List<String>
}
@JsonPropertyOrder(TITLE, CUSTOM_NAME, TAGS, TASK_LIST, TYPE)
abstract class LocalLessonMixin {
@JsonProperty(TITLE)
private lateinit var name: String
@JsonProperty(CUSTOM_NAME)
@JsonInclude(JsonInclude.Include.NON_NULL)
private var customPresentableName: String? = null
@JsonProperty(TASK_LIST)
private lateinit var _items: List<StudyItem>
val itemType: String
@JsonProperty(TYPE)
get() = throw NotImplementedInMixin()
@JsonProperty(TAGS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var contentTags: List<String>
}
@JsonPropertyOrder(
NAME, CUSTOM_NAME, TAGS, FILES, DESCRIPTION_TEXT, DESCRIPTION_FORMAT, FEEDBACK_LINK, SOLUTION_HIDDEN,
TASK_TYPE
)
abstract class LocalTaskMixin {
@JsonProperty(NAME)
private lateinit var name: String
@JsonProperty(FILES)
private lateinit var _taskFiles: MutableMap<String, TaskFile>
@JsonProperty(DESCRIPTION_TEXT)
private lateinit var descriptionText: String
@JsonProperty(DESCRIPTION_FORMAT)
private lateinit var descriptionFormat: DescriptionFormat
@JsonProperty(FEEDBACK_LINK)
@JsonInclude(JsonInclude.Include.NON_NULL)
private lateinit var feedbackLink: String
@JsonProperty(CUSTOM_NAME)
@JsonInclude(JsonInclude.Include.NON_NULL)
private var customPresentableName: String? = null
@JsonProperty(SOLUTION_HIDDEN)
@JsonInclude(JsonInclude.Include.NON_NULL)
private var solutionHidden: Boolean? = null
val itemType: String
@JsonProperty(TASK_TYPE)
get() = throw NotImplementedInMixin()
@JsonProperty(TAGS)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private lateinit var contentTags: List<String>
}
@JsonPropertyOrder(
CHOICE_OPTIONS, IS_MULTIPLE_CHOICE, MESSAGE_CORRECT, MESSAGE_INCORRECT, QUIZ_HEADER,
NAME, CUSTOM_NAME, TAGS, FILES, DESCRIPTION_TEXT, DESCRIPTION_FORMAT, FEEDBACK_LINK, SOLUTION_HIDDEN, TASK_TYPE
)
abstract class ChoiceTaskLocalMixin : LocalTaskMixin() {
@JsonProperty
private var isMultipleChoice: Boolean = false
@JsonProperty
private lateinit var choiceOptions: List<ChoiceOption>
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = FeedbackCorrectFilter::class)
@JsonProperty
private lateinit var messageCorrect: String
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = FeedbackIncorrectFilter::class)
@JsonProperty
private lateinit var messageIncorrect: String
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = QuizHeaderFilter::class)
@JsonProperty
private lateinit var quizHeader: String
}
abstract class ChoiceOptionLocalMixin {
@JsonProperty
private var text: String = ""
@JsonProperty
private var status: ChoiceOptionStatus = ChoiceOptionStatus.UNKNOWN
}
@JsonPropertyOrder(NAME, IS_VISIBLE, TEXT, IS_BINARY, IS_EDITABLE, HIGHLIGHT_LEVEL)
@JsonDeserialize(builder = EduFileBuilder::class)
abstract class EduFileMixin {
@JsonProperty(NAME)
private lateinit var name: String
@JsonProperty(IS_VISIBLE)
var isVisible: Boolean = true
lateinit var text: String
@JsonProperty(TEXT)
@Encrypt
set
var isBinary: Boolean? = null
@JsonProperty(IS_BINARY)
@JsonInclude(JsonInclude.Include.NON_NULL)
get
@JsonProperty(IS_BINARY)
@JsonInclude(JsonInclude.Include.NON_NULL)
set
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = TrueValueFilter::class)
@JsonProperty(IS_EDITABLE)
var isEditable: Boolean = true
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = HighlightLevelValueFilter::class)
@JsonProperty(HIGHLIGHT_LEVEL)
var errorHighlightLevel: EduFileErrorHighlightLevel = EduFileErrorHighlightLevel.TEMPORARY_SUPPRESSION
}
@JsonPropertyOrder(NAME, PLACEHOLDERS, IS_VISIBLE, TEXT, IS_BINARY, IS_EDITABLE, HIGHLIGHT_LEVEL)
@JsonDeserialize(builder = TaskFileBuilder::class)
abstract class TaskFileMixin : EduFileMixin() {
@JsonProperty(PLACEHOLDERS)
private lateinit var _answerPlaceholders: List<AnswerPlaceholder>
}
@JsonPropertyOrder(OFFSET, LENGTH, DEPENDENCY, PLACEHOLDER_TEXT, IS_VISIBLE)
@JsonDeserialize(converter = AnswerPlaceholderConverter::class)
abstract class AnswerPlaceholderMixin {
@JsonProperty(OFFSET)
private var offset: Int = -1
@JsonProperty(LENGTH)
private var length: Int = -1
@JsonProperty(DEPENDENCY)
@JsonInclude(JsonInclude.Include.NON_NULL)
private lateinit var placeholderDependency: AnswerPlaceholderDependency
@JsonProperty(PLACEHOLDER_TEXT)
private lateinit var placeholderText: String
@JsonProperty(IS_VISIBLE)
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = TrueValueFilter::class)
private var isVisible: Boolean = true
}
@JsonPropertyOrder(OFFSET, LENGTH, DEPENDENCY, POSSIBLE_ANSWER, PLACEHOLDER_TEXT)
abstract class AnswerPlaceholderWithAnswerMixin : AnswerPlaceholderMixin() {
@JsonProperty(POSSIBLE_ANSWER)
@Encrypt
private lateinit var possibleAnswer: String
}
@JsonInclude(JsonInclude.Include.NON_NULL)
abstract class AnswerPlaceholderDependencyMixin {
@JsonProperty(SECTION)
private lateinit var sectionName: String
@JsonProperty(LESSON)
private lateinit var lessonName: String
@JsonProperty(TASK)
private lateinit var taskName: String
@JsonProperty(FILE)
private lateinit var fileName: String
@JsonProperty(PLACEHOLDER)
private var placeholderIndex: Int = -1
@JsonProperty(IS_VISIBLE)
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = TrueValueFilter::class)
private var isVisible = true
}
@Suppress("unused")
abstract class EduTestInfoMixin {
@JsonProperty(NAME)
private lateinit var name: String
@JsonProperty(STATUS)
private var status: Int = -1
}
class CourseDeserializer : StdDeserializer<Course>(Course::class.java) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext?): Course? {
val node: ObjectNode = jp.codec.readTree(jp) as ObjectNode
return deserializeCourse(node, jp.codec)
}
private fun deserializeCourse(jsonObject: ObjectNode, codec: ObjectCodec): Course? {
if (jsonObject.has(COURSE_TYPE)) {
val courseType = jsonObject.get(COURSE_TYPE).asText()
val course = codec.treeToValue(jsonObject, EduCourse::class.java)
if (courseType == MARKETPLACE) {
course.isMarketplace = true
}
return course
}
return codec.treeToValue(jsonObject, EduCourse::class.java)
}
}
class StudyItemDeserializer : StdDeserializer<StudyItem>(StudyItem::class.java) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext?): StudyItem? {
val node: ObjectNode = jp.codec.readTree(jp) as ObjectNode
return deserializeItem(node, jp.codec)
}
private fun deserializeItem(jsonObject: ObjectNode, codec: ObjectCodec): StudyItem? {
if (jsonObject.has(TASK_TYPE)) {
val taskType = jsonObject.get(TASK_TYPE).asText()
return deserializeTask(jsonObject, taskType, codec)
}
return if (!jsonObject.has(ITEM_TYPE)) {
codec.treeToValue(jsonObject, Lesson::class.java)
}
else {
val itemType = jsonObject.get(ITEM_TYPE).asText()
when (itemType) {
LESSON -> codec.treeToValue(jsonObject, Lesson::class.java)
FRAMEWORK_TYPE -> codec.treeToValue(jsonObject, FrameworkLesson::class.java)
SECTION -> codec.treeToValue(jsonObject, Section::class.java)
else -> throw IllegalArgumentException("Unsupported item type: $itemType")
}
}
}
}
fun deserializeTask(node: ObjectNode, taskType: String, objectMapper: ObjectCodec): Task? {
return when (taskType) {
IdeTask.IDE_TASK_TYPE -> objectMapper.treeToValue(node, IdeTask::class.java)
ChoiceTask.CHOICE_TASK_TYPE -> objectMapper.treeToValue(node, ChoiceTask::class.java)
TheoryTask.THEORY_TASK_TYPE -> objectMapper.treeToValue(node, TheoryTask::class.java)
CodeTask.CODE_TASK_TYPE -> objectMapper.treeToValue(node, CodeTask::class.java)
// deprecated: old courses have pycharm tasks
EduTask.EDU_TASK_TYPE, EduTask.PYCHARM_TASK_TYPE -> {
objectMapper.treeToValue(node, EduTask::class.java)
}
OutputTask.OUTPUT_TASK_TYPE -> objectMapper.treeToValue(node, OutputTask::class.java)
MatchingTask.MATCHING_TASK_TYPE -> objectMapper.treeToValue(node, MatchingTask::class.java)
RemoteEduTask.REMOTE_EDU_TASK_TYPE -> objectMapper.treeToValue(node, RemoteEduTask::class.java)
SortingTask.SORTING_TASK_TYPE -> objectMapper.treeToValue(node, SortingTask::class.java)
UnsupportedTask.UNSUPPORTED_TASK_TYPE -> objectMapper.treeToValue(node, UnsupportedTask::class.java)
else -> {
LOG.warning("Unsupported task type $taskType")
null
}
}
}
@JsonPOJOBuilder(withPrefix = "")
private open class EduFileBuilder {
private var _name: String = ""
var name: String
@JsonProperty(NAME)
set(value) {
_name = value
}
@JsonProperty(NAME)
get() = _name
@JsonProperty(IS_VISIBLE)
var isVisible: Boolean = true
@JsonProperty(TEXT)
@Encrypt
var text: String? = null
@JsonProperty(IS_BINARY)
var isBinary: Boolean? = null
@JsonProperty(IS_EDITABLE)
var isEditable: Boolean = true
@JsonProperty
var isPropagatable: Boolean = true
@JsonProperty(HIGHLIGHT_LEVEL)
var errorHighlightLevel: EduFileErrorHighlightLevel = EduFileErrorHighlightLevel.TEMPORARY_SUPPRESSION
@JacksonInject(FILE_CONTENTS_FACTORY_INJECTABLE_VALUE)
var fileContentsFactory: FileContentsFactory = EmtpyFileContentFactory
private fun build(): EduFile {
val result = EduFile()
updateFile(result)
return result
}
protected fun updateFile(result: EduFile) {
result.name = name
result.isVisible = isVisible
result.isEditable = isEditable
result.isPropagatable = isPropagatable
result.errorHighlightLevel = errorHighlightLevel
val text = this.text
result.contents = if (text != null) {
// The "text" field is not allowed starting from the 19th version of the format.
// But we have this branch here because it is used when reading an older version of the course.json
when (isBinary) {
true -> InMemoryBinaryContents.parseBase64Encoding(text)
false -> InMemoryTextualContents(text)
null -> InMemoryUndeterminedContents(text)
}
}
else {
when (isBinary) {
true -> fileContentsFactory.createBinaryContents(result)
false -> fileContentsFactory.createTextualContents(result)
null -> throw IllegalStateException("If the text field is absent, it must contain file binarity")
}
}
}
}
@JsonPOJOBuilder(withPrefix = "")
private class TaskFileBuilder : EduFileBuilder() {
@JsonProperty(PLACEHOLDERS)
var answerPlaceholders: List<AnswerPlaceholder> = mutableListOf()
private fun build(): TaskFile {
val result = TaskFile()
updateFile(result)
result.answerPlaceholders = answerPlaceholders
return result
}
}
class AnswerPlaceholderConverter : StdConverter<AnswerPlaceholder, AnswerPlaceholder?>() {
override fun convert(value: AnswerPlaceholder): AnswerPlaceholder =
value.apply { takeIsVisibleFromDependency() }
}
fun AnswerPlaceholder.takeIsVisibleFromDependency() {
val dependency = placeholderDependency ?: return
if (!dependency.isVisible) {
dependency.isVisible = true
isVisible = false
}
}