Skip to content

[MBL-2883] RichText parsing and conversion to Swift-friendly normalized types#2810

Open
stevestreza-ksr wants to merge 3 commits intomainfrom
stevestreza/sdui-content/parsing-rich-text
Open

[MBL-2883] RichText parsing and conversion to Swift-friendly normalized types#2810
stevestreza-ksr wants to merge 3 commits intomainfrom
stevestreza/sdui-content/parsing-rich-text

Conversation

@stevestreza-ksr
Copy link
Copy Markdown
Contributor

📲 What

Adds parsing, conversion, and types to turn GraphQL output into much easier-to-use block-level elements for RichText. These will be used for rendering.

🤔 Why

Due to quirks in how GraphQL types are laid out, there are 40 different Swift types that get generated by Apollo. We need something a little less crazy to use in the client.

🛠 How

A RichTextElement enum type was added which represents the different types that can be in GraphQL RichText in a normalized form that can be easily handed to SwiftUI for rendering.

Then, a whoooooole bunch of converter functions were added as extensions to those 40 types to turn the GraphQL specific types into their RichTextElement types.

And some tests.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a normalized RichTextElement model and a set of GraphAPI-to-model conversion helpers, with tests, to make Apollo-generated RichText output easier to consume for SwiftUI rendering.

Changes:

  • Introduces RichTextElement (normalized block-level RichText representation).
  • Adds conversion extensions on RichTextComponentFragment/nested item types to produce RichTextElement values.
  • Adds conversion-focused tests using Swift Testing.

Reviewed changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated 10 comments.

File Description
ServerDrivenUI/Sources/ServerDrivenUI/RichTextElement.swift Defines the normalized RichText element enum + associated value types (text/media/oembed).
ServerDrivenUI/Sources/ServerDrivenUI/RichTextGQLConversions.swift Implements GraphQL fragment/type conversion into RichTextElement.
ServerDrivenUI/Tests/ServerDrivenUITests/RichTextGQLConversionTests.swift Adds unit tests validating conversions across the supported GraphQL RichText variants.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +66
public enum HeaderLevel: String {
case one = "HEADING_1"
case two = "HEADING_2"
case three = "HEADING_3"
case four = "HEADING_4"

init?(_ rawValues: [String]?) {
for rawValue in rawValues ?? [] {
if let level = HeaderLevel(rawValue: rawValue) {
self = level
return
}
}
return nil
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HeaderLevel.init(_:) introduces behavior (finding the first heading style in the styles array) but the new test suite doesn’t cover any inputs containing HEADING_1HEADING_4. Add a test that ensures the expected header level is produced when heading styles are present (and, if relevant, which one wins when multiple heading styles appear).

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +12
extension RichTextComponentFragment {
public func asRichTextElements() -> [RichTextElement] {
self.items.map { $0.asRichTextElement() }
}
}

extension RichTextComponentFragment.Item {
func asRichTextElement() -> RichTextElement {
if let x = asRichText { return x.asRichTextElement }
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asRichTextElement is a method on RichTextComponentFragment.Item/Child but a computed property on the concrete GraphQL subtype structs. This inconsistency makes call sites harder to follow and encourages mixed usage. Consider standardizing on either a method or a property across all these conversions (and aligning asRichTextElements() accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +22
if let x = asRichTextPhoto { return x.asRichTextElement }
if let x = asRichTextAudio { return x.asRichTextElement }
if let x = asRichTextVideo { return x.asRichTextElement }
if let x = asRichTextOembed { return x.asRichTextElement }
return .text(makeText(text: nil, link: nil, styles: nil, children: []), nil)
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When none of the asRichText* cases match, this falls back to an empty .text element. That will silently drop/obscure unexpected GraphQL union cases (e.g., when the schema adds a new RichText item) and can make rendering bugs hard to diagnose. Consider surfacing this explicitly (assert/log in debug, or add an .unknown case / return optional).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Copilot here - I like the idea of an .unknown case or adding an assert.

Copy link
Copy Markdown
Contributor

@amy-at-kickstarter amy-at-kickstarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some non-blocking nits and questions.

Comment on lines +17 to +22
if let x = asRichTextPhoto { return x.asRichTextElement }
if let x = asRichTextAudio { return x.asRichTextElement }
if let x = asRichTextVideo { return x.asRichTextElement }
if let x = asRichTextOembed { return x.asRichTextElement }
return .text(makeText(text: nil, link: nil, styles: nil, children: []), nil)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Copilot here - I like the idea of an .unknown case or adding an assert.

if let x = asRichTextAudio { return x.asRichTextElement }
if let x = asRichTextVideo { return x.asRichTextElement }
if let x = asRichTextOembed { return x.asRichTextElement }
return .text(makeText(text: nil, link: nil, styles: nil, children: []), nil)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: given that .text is such a generic name, I would find these easier to skim if you qualified them like

Suggested change
return .text(makeText(text: nil, link: nil, styles: nil, children: []), nil)
return RichTextElement.text(makeText(text: nil, link: nil, styles: nil, children: []), nil)

import Foundation
import GraphAPI
@testable import ServerDrivenUI
import Testing
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like SDUI is the first place we're using Swift testing instead of XCTest. Seems like a good thing, but two requests:

  1. Can you double-check that it's playing nicely with CircleCI?
  2. Can you give a quick demo/rundown for how we're going to use it in a future iOS sync?

@nativeksr
Copy link
Copy Markdown
Collaborator

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants