From 0399240435f7f2c688a3db3691439e1b0634c73e Mon Sep 17 00:00:00 2001 From: Naftoli Gugenheim <98384+nafg@users.noreply.github.com> Date: Thu, 14 Dec 2023 01:24:12 -0500 Subject: [PATCH] Cover more of MUI Includes improvements to overrides.yml such as support for extends and defining new components --- overrides.yml | 154 +++++++++++++++++++++------- project/FacadeGenerator.scala | 69 +++++++++---- project/FacadeGeneratorPlugin.scala | 14 +-- project/Overrides.scala | 42 ++++++-- project/PropTypeInfo.scala | 42 +++++--- 5 files changed, 230 insertions(+), 91 deletions(-) diff --git a/overrides.yml b/overrides.yml index 77c7c56..15d1829 100644 --- a/overrides.yml +++ b/overrides.yml @@ -28,24 +28,81 @@ mui: - ReactMouseEventFromHtml result: callback components: + .system: + props: + alignItems: + type: + base: string + presets: + - string: flex-start + - string: flex-end + - string: center + - string: stretch + - string: baseline + color: + type: + base: string + presets: + - string: primary.main + - string: secondary.main + - string: error.main + - string: warning.main + - string: info.main + - string: success.main + - string: text.primary + - string: text.secondary + - string: text.disabled + display: + type: + base: string + presets: + - string: block + - string: inline + justifyContent: + type: + base: string + presets: + - string: flex-start + - string: flex-end + - string: center + - string: space-between + - string: space-around + - string: space-evenly + Alert: + props: + icon: + type: + base: + anyOf: + - bool + - vdomNode + presets: [ false ] Autocomplete: props: + filterOptions: + type: + args: + - arrayOf: jsAny + - jsObject + result: + arrayOf: jsAny getOptionLabel: type: args: - jsAny result: string - onChange: + isOptionEqualToValue: type: args: - - ReactEvent - jsAny - result: callback - renderOption: + - jsAny + result: bool + onChange: type: args: + - ReactEvent - jsAny - result: vdomNode + result: callback onInputChange: type: args: @@ -58,17 +115,25 @@ mui: args: - jsDictionary result: vdomNode - filterOptions: + renderOption: type: - args: - - arrayOf: jsAny - - jsObject - result: - arrayOf: jsAny + args: [ jsObject, jsAny, jsObject, jsObject ] + result: vdomNode Breadcrumbs: moduleTrait: FacadeModule.ArrayChildren ButtonGroup: moduleTrait: FacadeModule.ArrayChildren + Card: + extends: Paper + props: + raised: + type: bool + Chip: + props: + onDelete: + type: + args: [ ReactMouseEvent ] + result: callback Container: props: children: @@ -88,18 +153,12 @@ mui: - string - bool presets: - - name: xs - code: '"xs"' - - name: sm - code: '"sm"' - - name: md - code: '"md"' - - name: lg - code: '"lg"' - - name: xl - code: '"xl"' - - name: "false" - code: 'false' + - string: xs + - string: sm + - string: md + - string: lg + - string: xl + - false Dialog: props: onClose: @@ -114,14 +173,17 @@ mui: type: base: string presets: - - name: inherit - code: '"inherit"' - - name: large - code: '"large"' - - name: medium - code: '"medium"' - - name: small - code: '"small"' + - string: inherit + - string: large + - string: medium + - string: small + Fade: + props: + children: + required: true + type: vdomElement + Grid: + extends: .system IconButton: props: children: @@ -133,6 +195,8 @@ mui: args: - ReactEventFromInput result: callback + Link: + extends: Typography List: moduleTrait: FacadeModule.ArrayChildren ListItem: @@ -178,18 +242,24 @@ mui: anyOf: - element - jsObject + RadioGroup: + extends: FormGroup + Select: + props: + renderValue: + type: + args: + - jsAny + result: vdomNode TableCell: props: padding: type: base: string presets: - - name: normal - code: '"normal"' - - name: checkbox - code: '"checkbox"' - - name: none - code: '"none"' + - string: normal + - string: checkbox + - string: none TablePagination: props: page: @@ -204,6 +274,14 @@ mui: - ReactEvent - int result: callback + Tabs: + props: + onChange: + type: + args: + - ReactEvent + - jsAny + result: callback TextField: props: onChange: @@ -227,6 +305,8 @@ mui: children: type: vdomElement required: true + Typography: + extends: .system mui.lab: common: diff --git a/project/FacadeGenerator.scala b/project/FacadeGenerator.scala index 1d70943..6179941 100644 --- a/project/FacadeGenerator.scala +++ b/project/FacadeGenerator.scala @@ -151,8 +151,8 @@ object FacadeGenerator { s"""object ${propInfo.identifier} extends PropTypes.Prop[$typeCode]("${propInfo.identifier}") { |${ propInfo.`type`.presets - .map { case PropTypeInfo.Preset(name, presetCode) => - s" val $name = this := $presetCode.asInstanceOf[$typeCode]" + .map { value => + s" val ${value.name} = this := ${value.code}.asInstanceOf[$typeCode]" } .mkString("\n") } @@ -219,24 +219,53 @@ object FacadeGenerator { val readComponentInfos = ujson.read(docgenOutput).obj.values.toSeq.collect { case value if Set("props", "displayName").forall(value.obj.contains) => - val componentInfo = ComponentInfo.read(value.obj) - overrides.getPropInfoOverrides(componentInfo).foldLeft(componentInfo) { - case (componentInfo, (propName, Overrides.PropInfoOverride(typ, required))) => - componentInfo.propsMap.get(propName) - .map { existingPropInfo => - existingPropInfo.copy( - required = required.getOrElse(existingPropInfo.required), - `type` = typ.getOrElse(existingPropInfo.`type`) - ) - } - .orElse(typ.map(propTypeInfo => PropInfo(propName, propTypeInfo))) - .fold(componentInfo)(componentInfo.withProp) - } + ComponentInfo.read(value.obj) } - val componentInfos = - readComponentInfos ++ - (overrides.components -- readComponentInfos.map(_.name).toSet).map { case (name, overrides) => + def applyExtends(componentInfo: ComponentInfo): ComponentInfo = { + val name = componentInfo.name + overrides.get(name).`extends` match { + case None => + logger.info("No extends for " + name) + componentInfo + case Some(parentName) => + readComponentInfos.find(_.name == parentName) + .fold(componentInfo) { parent => + logger.info("Extending " + name + " with " + parentName) + val parentExtended = applyExtends(parent) + val map = parentExtended.propsMap ++ componentInfo.propsMap + logger.info("Added props " + (map.keySet -- parentExtended.propsMap.keySet).mkString(", ") + " to " + + name) + componentInfo.copy(propsMap = map) + } + } + } + + val inheritedExisting = readComponentInfos.map { componentInfo => + logger.info(componentInfo.name) + applyExtends(componentInfo) + } + + val overriddenExisting = inheritedExisting.map { componentInfo => + overrides.getPropInfoOverrides(componentInfo).foldLeft(componentInfo) { + case (componentInfo, (propName, Overrides.PropInfoOverride(typ, required))) => + componentInfo.propsMap.get(propName) + .map { existingPropInfo => + existingPropInfo.copy( + required = required.getOrElse(existingPropInfo.required), + `type` = typ.getOrElse(existingPropInfo.`type`) + ) + } + .orElse(typ.map(propTypeInfo => PropInfo(propName, propTypeInfo))) + .fold(componentInfo)(componentInfo.withProp) + } + } + + val readComponentNames = overriddenExisting.map(_.name).toSet + + val addedComponentInfos = + (overrides.components -- readComponentNames).collect { + case (name, overrides) if name.headOption.exists(_.isLetter) => ComponentInfo( name = name, description = "", @@ -249,7 +278,9 @@ object FacadeGenerator { ) } ) - } + } + + val componentInfos = overriddenExisting ++ addedComponentInfos for (componentInfo <- componentInfos) yield processComponent( info = componentInfo, diff --git a/project/FacadeGeneratorPlugin.scala b/project/FacadeGeneratorPlugin.scala index 76404e0..a72b80e 100644 --- a/project/FacadeGeneratorPlugin.scala +++ b/project/FacadeGeneratorPlugin.scala @@ -1,5 +1,3 @@ -import java.io.FileReader - import _root_.io.circe.yaml.v12.Parser import cats.data.Validated import cats.implicits.toShow @@ -22,19 +20,12 @@ object FacadeGeneratorPlugin extends AutoPlugin { runYarnInstall.value val logger = streams.value.log val overrides = - Parser.default.parse(new FileReader("overrides.yml")).toTry.get - .asAccumulating[Map[String, Overrides]] match { + Parser.default.decodeAccumulating[Map[String, Overrides]](IO.read(file("overrides.yml"))) match { case Validated.Invalid(errors) => errors.toList.foreach(failure => logger.error(failure.show)) sys.error(errors.toString) - case Validated.Valid(map) => map(scalaSubPackage) + case Validated.Valid(map) => map(scalaSubPackage).applyExtends } - val overridesString = pprint.apply(overrides).render - logger.info( - overridesString.linesWithSeparators - .map(s"[${scalaSubPackage}] " + _) - .mkString - ) FacadeGenerator.run( base = os.Path((Compile / sourceManaged).value), repoDir = reactDocGenDir.value, @@ -76,7 +67,6 @@ object FacadeGeneratorPlugin extends AutoPlugin { .call(cwd = dir, stderr = os.Inherit, stdout = os.Inherit) } }, - watchSources += file("overrides.yml"), Compile / packageSrc / mappings ++= { val base = (Compile / sourceManaged).value val files = (Compile / managedSources).value diff --git a/project/Overrides.scala b/project/Overrides.scala index c4297c9..dc36077 100644 --- a/project/Overrides.scala +++ b/project/Overrides.scala @@ -1,3 +1,5 @@ +import scala.collection.immutable.SortedMap + import Overrides.PropInfoOverride import io.circe.Codec import io.circe.generic.extras.Configuration @@ -5,18 +7,46 @@ import io.circe.generic.extras.semiauto.deriveConfiguredCodec case class Overrides( - common: Map[String, PropInfoOverride] = Map.empty, - components: Map[String, Overrides.ComponentOverrides] = Map.empty, - moduleTraits: Map[String, String] = Map.empty) { + common: SortedMap[String, PropInfoOverride] = SortedMap.empty, + components: SortedMap[String, Overrides.ComponentOverrides] = SortedMap.empty) { + + def get(name: String): Overrides.ComponentOverrides = components.getOrElse(name, Overrides.ComponentOverrides.empty) def getPropInfoOverrides(componentInfo: ComponentInfo): Map[String, PropInfoOverride] = common ++ - components.getOrElse(componentInfo.name, Overrides.ComponentOverrides.NoOp).props + get(componentInfo.name).props + + def applyExtends: Overrides = + copy(components = + components.map { case (name, componentOverrides) => + name -> componentOverrides.applyExtends(this) + } + ) } object Overrides { - case class ComponentOverrides(props: Map[String, PropInfoOverride] = Map.empty, moduleTrait: Option[String] = None) + case class ComponentOverrides( + props: SortedMap[String, PropInfoOverride] = SortedMap.empty, + `extends`: Option[String] = None, + moduleTrait: Option[String] = None) { + + def ++(that: ComponentOverrides) = + copy( + props = this.props ++ that.props, + `extends` = this.`extends`.orElse(that.`extends`), + moduleTrait = this.moduleTrait.orElse(that.moduleTrait) + ) + + def applyExtends(overrides: Overrides): ComponentOverrides = + `extends` match { + case None => this + case Some(parentName) => + val parent = overrides.get(parentName) + val parentExtended = parent.applyExtends(overrides) + parentExtended ++ this + } + } object ComponentOverrides { - val NoOp = ComponentOverrides() + val empty = ComponentOverrides() } case class PropInfoOverride(`type`: Option[PropTypeInfo] = None, required: Option[Boolean] = None) diff --git a/project/PropTypeInfo.scala b/project/PropTypeInfo.scala index 39e63d6..63cd6d7 100644 --- a/project/PropTypeInfo.scala +++ b/project/PropTypeInfo.scala @@ -18,7 +18,7 @@ sealed trait PropTypeInfo { def sequence: PropTypeInfo = PropTypeInfo.Simple.Sequence(this) - def |(that: PropTypeInfo) = PropTypeInfo.Union(Seq(this, that)).flatten + def |(that: PropTypeInfo) = PropTypeInfo.Union(Seq(this, that)).simplify } object PropTypeInfo { private implicit val circeConfig = @@ -102,11 +102,11 @@ object PropTypeInfo { override def presets = Nil } case class Union(anyOf: Seq[PropTypeInfo]) extends PropTypeInfo { - override def code = anyOf.map(_.safeCode).mkString(" | ") + override def code = anyOf.map(_.safeCode).distinct.mkString(" | ") override def safeCode = s"($code)" override def imports = anyOf.flatMap(_.imports).toSet ++ CommonImports.| override def presets = anyOf.flatMap(_.presets) - def flatten = + def simplify = copy(anyOf = anyOf .flatMap { @@ -122,10 +122,20 @@ object PropTypeInfo { override def imports = base.imports } - case class Preset(name: Identifier, code: String) + sealed abstract class Preset(val name: Identifier, val code: String) object Preset { - def literal(value: String) = Preset(Identifier(value), value) - def string(value: String) = Preset(Identifier(value), '"' + value + '"') + case class Unquoted(value: String) extends Preset(Identifier(value), value) + case class Quoted(string: String) extends Preset(Identifier(string), '"' + string + '"') + + implicit val codecQuoted: Codec[Quoted] = deriveConfiguredCodec + implicit val encodePreset: Encoder[Preset] = Encoder.instance[Preset] { + case u: Unquoted => u.value.asJson + case q: Quoted => q.asJson + } + implicit val decodePreset: Decoder[Preset] = + codecQuoted + .or(Decoder.decodeString.map[Preset](Unquoted)) + .or(Decoder.decodeBoolean.map(b => Unquoted(b.toString))) } val int: PropTypeInfo = Simple.int @@ -141,7 +151,6 @@ object PropTypeInfo { val vdomNode: PropTypeInfo = Simple.vdomNode val vdomElement: PropTypeInfo = Simple.vdomElement val element: PropTypeInfo = Simple.element - def stringEnum(values: String*): PropTypeInfo = WithPresets(Simple.string, values.map(Preset.string)) private val stringEnumValueRE = "'(.*)'".r private val litEnumValueRE = """(true|false|-?\d+\.\d+|-?\d+)""".r @@ -151,27 +160,26 @@ object PropTypeInfo { case simple: PropType.Simple => Simple.fromPropType(simple) case PropType.Func => jsAny =>: jsAny case PropType.ArrayOf(param) => apply(param).sequence - case PropType.Union(types) => Union(types.map(apply)) + case PropType.Union(types) => Union(types.map(apply)).simplify case PropType.Enum(base, values) => - WithPresets(Simple.fromPropType(base), + WithPresets(apply(base), values.collect { - case litEnumValueRE(s) => Preset.literal(s) - case stringEnumValueRE(s) => Preset.string(s) + case litEnumValueRE(s) => Preset.Unquoted(s) + case stringEnumValueRE(s) => Preset.Quoted(s) } ) } - private implicit val presetCodec: Codec[PropTypeInfo.Preset] = deriveConfiguredCodec - private implicit val enumCodec: Codec[PropTypeInfo.WithPresets] = deriveConfiguredCodec - private implicit val unionCodec: Codec[PropTypeInfo.Union] = deriveConfiguredCodec - private implicit val functionCodec: Codec[PropTypeInfo.Function] = deriveConfiguredCodec - implicit val encodePropTypeInfo: Encoder[PropTypeInfo] = Encoder.instance[PropTypeInfo] { + implicit val enumCodec: Codec[PropTypeInfo.WithPresets] = deriveConfiguredCodec + implicit val unionCodec: Codec[PropTypeInfo.Union] = deriveConfiguredCodec + implicit val functionCodec: Codec[PropTypeInfo.Function] = deriveConfiguredCodec + implicit val encodePropTypeInfo: Encoder[PropTypeInfo] = Encoder.instance[PropTypeInfo] { case s: Simple => Simple.encodeSimple(s) case e: WithPresets => enumCodec(e) case u: Union => unionCodec(u) case f: Function => functionCodec(f) } - implicit val decodePropTypeInfo: Decoder[PropTypeInfo] = + implicit val decodePropTypeInfo: Decoder[PropTypeInfo] = unionCodec .or(enumCodec.map(identity[PropTypeInfo])) .or(functionCodec.map(identity[PropTypeInfo]))