Skip to content

feat(figma): REST API client, URL parser, normalization (Phase B — T2F.0–T2F.4)#40

Open
canonical-muhammadbassiony wants to merge 3 commits into
feat/phase-2-cli-featuresfrom
feat/figma-phase-b-client
Open

feat(figma): REST API client, URL parser, normalization (Phase B — T2F.0–T2F.4)#40
canonical-muhammadbassiony wants to merge 3 commits into
feat/phase-2-cli-featuresfrom
feat/figma-phase-b-client

Conversation

@canonical-muhammadbassiony

Copy link
Copy Markdown
Collaborator

Summary

Introduces the internal/figma package with URL parsing, REST API client, raw types, and a normalization layer. This is the foundation for all Figma integration.

Tasks Implemented

  • T2F.0: Local development setup verification — verify-figma Taskfile command
  • T2F.1: URL parser (ParseLink) — extracts file key and node ID from Figma /file/ and /design/ URLs
  • T2F.2: --figma-url CLI flag + Config.Validate() for token requirement
  • T2F.3: REST API client — GetMeta, GetNodes, GetComments, GetImages, DownloadImage with structured error handling (401/403/429/404)
  • T2F.4: Normalization layer — Normalize() converts raw API types into NormalizedDesign with DesignAnchor, DesignComment, ScreenshotArtifact

Files Changed

  • internal/figma/link.go + link_test.go — URL parser with table-driven tests
  • internal/figma/types.go — raw API types + Bauer-owned normalized types
  • internal/figma/client.go + client_test.go — REST client with mock HTTP tests
  • internal/figma/normalize.go + normalize_test.go — normalization logic
  • internal/source/types.goDesign field typed as *figma.NormalizedDesign
  • internal/source/manager.goFetchFigma method
  • internal/config/config.goValidate() checks Figma token presence

Part of the Bauer v2 stacked PR series (Branch 5 of 12).

- T2F.0: verify-figma Taskfile task, .env.example Figma vars
- T2F.1: internal/figma/link.go - ParseLink for /file/ and /design/ URLs
- T2F.2: FigmaToken/FigmaURL config wiring, FIGMA_TOKEN fallback
- T2F.3: internal/figma/client.go - GetMeta, GetNodes, GetComments, GetImages, DownloadImage
- T2F.4: internal/figma/normalize.go - Normalize() into NormalizedDesign

Copilot AI left a comment

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.

Pull request overview

Adds an initial internal/figma integration layer (URL parsing, REST client, raw/normalized types, normalization + tests) and begins wiring Figma into the source/config plumbing to serve as the foundation for later Figma→prompt mapping.

Changes:

  • Introduces internal/figma with ParseLink, REST client methods, raw API types, and a normalization layer (+ unit tests).
  • Updates SourceBundle.Design to *figma.NormalizedDesign and adds source.Manager.FetchFigma.
  • Adds config validation for requiring a Figma token when a Figma URL is provided, plus a verify-figma Taskfile helper.

Reviewed changes

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

Show a summary per file
File Description
Taskfile.yml Adds/adjusts verify-figma task output and error handling.
internal/source/types.go Types Design as *figma.NormalizedDesign instead of any.
internal/source/manager.go Adds FetchFigma method to fetch meta/nodes/comments/images and normalize.
internal/figma/types.go Defines raw Figma API response structs and Bauer normalized types.
internal/figma/normalize.go Normalizes meta/nodes/comments/screenshot paths into NormalizedDesign.
internal/figma/normalize_test.go Unit tests for normalization behavior.
internal/figma/link.go Parses Figma URLs into LinkRef (file key + optional node-id).
internal/figma/link_test.go Unit tests for URL parsing.
internal/figma/client.go REST API client for meta/nodes/comments/images and image downloads.
internal/figma/client_test.go HTTP-mocked unit tests for client methods and error handling.
internal/config/manager.go Plumbs FigmaURL through FlagsSource.Load().
internal/config/config.go Validates Figma token presence when FigmaURL is set.
internal/config/cli.go Adds --figma-url to this flag parser (but see PR comment about actual CLI entrypoint).
docs/implementation-log.md Marks Phase B branch as done and records summary/files.

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

Comment on lines +46 to +54
meta, err := client.GetMeta(ctx, ref.FileKey)
if err != nil {
return nil, fmt.Errorf("fetching figma metadata: %w", err)
}

nodeIDs := []string{}
if ref.NodeID != "" {
nodeIDs = []string{ref.NodeID}
}
Comment thread internal/figma/client.go
Comment on lines +81 to +83
// DownloadImage downloads a pre-signed image URL (no auth needed) to destPath.
// The file is created with 0o644 permissions.
func (c *Client) DownloadImage(ctx context.Context, presignedURL, destPath string) error {
Comment thread internal/figma/normalize.go Outdated
Comment on lines +73 to +77
// extractAnchors recursively extracts DesignAnchor values from a document node subtree.
// path is the breadcrumb from the root to the current node (used for NodePath).
func extractAnchors(nodeID string, doc *DocumentNode, path []string) []DesignAnchor {
currentPath := append(append([]string{}, path...), doc.Name)
anchor := DesignAnchor{
Comment thread internal/figma/link.go
Comment on lines +21 to +25
func ParseLink(rawURL string) (*LinkRef, error) {
matches := figmaFilePattern.FindStringSubmatch(rawURL)
if len(matches) < 2 {
return nil, fmt.Errorf("not a valid Figma link: %q (expected figma.com/file/... or figma.com/design/...)", rawURL)
}
Comment thread internal/config/cli.go
Comment on lines 45 to +46
artifactsDir := flag.String("artifacts-dir", "", "Directory for run artifacts (default: ./bauer-artifacts)")
figmaURL := flag.String("figma-url", "", "Figma file or design URL for design reference (optional)")

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.

Comment thread internal/figma/link.go
Comment on lines +22 to +24
u, err := url.Parse(rawURL)
if err != nil || (u.Host != "www.figma.com" && u.Host != "figma.com") {
return nil, fmt.Errorf("not a valid Figma link: %q (expected figma.com/file/... or figma.com/design/...)", rawURL)
Comment on lines +38 to +42
// Whole-file fetch: collect anchors from every node returned.
// Use a sorted iteration order if stability matters for tests.
for nodeID, entry := range nodes.Nodes {
design.Anchors = append(design.Anchors, extractAnchors(nodeID, &entry.Document, nil)...)
}
Comment on lines +59 to +68
// Map screenshot paths to ScreenshotArtifact records.
now := time.Now().UTC().Format(time.RFC3339)
for nodeID, localPath := range screenshotPaths {
design.Screenshots = append(design.Screenshots, ScreenshotArtifact{
NodeID: nodeID,
LocalPath: localPath,
Scale: 2,
FetchedAt: now,
})
}
Comment on lines +56 to +60
var nodes *figma.NodesResponse
if len(nodeIDs) == 0 {
fmt.Printf("warning: whole-file Figma link — no specific node requested, skipping node fetch\n")
nodes = &figma.NodesResponse{}
} else {
Comment thread internal/source/manager.go Outdated
Comment on lines +82 to +86
continue
}
destPath := filepath.Join(screenshotDir, fmt.Sprintf("shot-node-%s.png", strings.ReplaceAll(nodeID, ":", "-")))
if err := client.DownloadImage(ctx, imgURL, destPath); err != nil {
fmt.Printf("warning: could not download screenshot for node %s: %v\n", nodeID, err)
Comment thread internal/figma/client.go Outdated
case http.StatusOK:
// success path; continue
case http.StatusUnauthorized, http.StatusForbidden:
return nil, fmt.Errorf("figma API authentication failed (status %d): check BAUER_FIGMA_TOKEN", resp.StatusCode)
Comment on lines +156 to +162
func (t *prefixTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the host to point to the test server.
req2 := req.Clone(req.Context())
req2.URL.Scheme = "http"
req2.URL.Host = t.base[len("http://"):]
return http.DefaultTransport.RoundTrip(req2)
}
Comment thread internal/figma/client.go
Comment on lines +81 to +109
// DownloadImage downloads a pre-signed image URL (no auth needed) to destPath.
// The file is created with default permissions (0666 & ~umask).
func (c *Client) DownloadImage(ctx context.Context, presignedURL, destPath string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, presignedURL, nil)
if err != nil {
return fmt.Errorf("creating download request: %w", err)
}

resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("downloading image: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("image download failed: status %d", resp.StatusCode)
}

f, err := os.Create(destPath)
if err != nil {
return fmt.Errorf("creating image file: %w", err)
}
defer f.Close()

if _, err := io.Copy(f, resp.Body); err != nil {
return fmt.Errorf("writing image: %w", err)
}
return nil
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — adding tests for DownloadImage is out of scope for this PR. Tracked as a follow-up.

Copilot AI left a comment

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.

Pull request overview

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

Comment thread internal/figma/types.go
ClientMeta CommentClientMeta `json:"client_meta"`
CreatedAt string `json:"created_at"`
User CommentUser `json:"user"`
ParentID string `json:"parent_id,omitempty"`
Comment thread internal/figma/link.go
Comment on lines +28 to +32
if !strings.EqualFold(host, "www.figma.com") && !strings.EqualFold(host, "figma.com") {
return nil, fmt.Errorf("not a valid Figma link: %q (expected figma.com/file/... or figma.com/design/...)", rawURL)
}

matches := figmaFilePattern.FindStringSubmatch(rawURL)
Comment thread internal/figma/client.go
case http.StatusTooManyRequests:
return nil, fmt.Errorf("figma API rate limit exceeded (status 429): retry after a delay")
case http.StatusNotFound:
return nil, fmt.Errorf("figma resource not found (status 404): check the file key and node ID")
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.

2 participants