diff --git a/models/registration/register.go b/models/registration/register.go index fce9df44..55cddd55 100644 --- a/models/registration/register.go +++ b/models/registration/register.go @@ -139,6 +139,30 @@ func (rh *RegistrationHelper) register(pkg PackagingUnit) { for _, rel := range pkg.Relationships { rel.Model = model.ToReference() rel.ModelId = model.Id + + if rel.Metadata != nil && rel.Metadata.Styles != nil { + svgComplete := "" + if rel.Metadata.Styles.SvgComplete != nil { + svgComplete = *rel.Metadata.Styles.SvgComplete + } + + var svgCompletePath string + + // Write SVG for relationships + rel.Metadata.Styles.SvgColor, rel.Metadata.Styles.SvgWhite, svgCompletePath = WriteAndReplaceSVGWithFileSystemPath( + rel.Metadata.Styles.SvgColor, + rel.Metadata.Styles.SvgWhite, + svgComplete, + rh.svgBaseDir, + model.Name, + string(rel.Kind), + false, + ) + if svgCompletePath != "" { + rel.Metadata.Styles.SvgComplete = &svgCompletePath + } + } + _, _, err := rh.regManager.RegisterEntity(model.Registrant, &rel) if err != nil { err = ErrRegisterEntity(err, string(rel.Type()), string(rel.Kind)) diff --git a/registry/component.go b/registry/component.go index fa9245ea..c7ac475e 100644 --- a/registry/component.go +++ b/registry/component.go @@ -1,12 +1,12 @@ package registry import ( + "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" - "encoding/json" "github.com/meshery/meshkit/encoding" "github.com/meshery/meshkit/files" @@ -14,10 +14,10 @@ import ( "github.com/meshery/meshkit/utils" "github.com/meshery/meshkit/utils/csv" "github.com/meshery/meshkit/utils/manifests" + "github.com/meshery/schemas" "github.com/meshery/schemas/models/v1alpha1/capability" schmeaVersion "github.com/meshery/schemas/models/v1beta1" "github.com/meshery/schemas/models/v1beta1/component" - "github.com/meshery/schemas" ) const ( @@ -209,8 +209,6 @@ func (c *ComponentCSV) UpdateCompDefinition(compDef *component.ComponentDefiniti return nil } - - type ComponentCSVHelper struct { SpreadsheetID int64 SpreadsheetURL string @@ -377,6 +375,84 @@ func CreateRelationshipsMetadata(model ModelCSV, relationships []RelationshipCSV } +// CreateRelationshipsMetadataAndCreateSVGsForMDStyle creates relationship metadata and writes SVGs for MD style docs +func CreateRelationshipsMetadataAndCreateSVGsForMDStyle(model ModelCSV, relationships []RelationshipCSV, path, svgDir string) (string, error) { + err := os.MkdirAll(filepath.Join(path), 0777) + if err != nil { + return "", err + } + relationshipMetadata := "" + for _, relnship := range relationships { + relationshipTemplate := ` +- type: %s + kind: %s + colorIcon: %s + whiteIcon: %s + description: %s` + + relnshipName := utils.FormatName(manifests.FormatToReadableString(fmt.Sprintf("%s-%s", relnship.KIND, relnship.SubType))) + colorIconDir := filepath.Join(svgDir, relnshipName, "icons", "color") + whiteIconDir := filepath.Join(svgDir, relnshipName, "icons", "white") + + relationshipMetadata += fmt.Sprintf(relationshipTemplate, relnship.Type, relnship.KIND, fmt.Sprintf("%s/%s-color.svg", colorIconDir, relnshipName), fmt.Sprintf("%s/%s-white.svg", whiteIconDir, relnshipName), relnship.Description) + + // Get SVGs for relationship + colorSVG, whiteSVG := getSVGForRelationship(model, relnship) + + // Only create directories and write SVGs if they exist + if colorSVG != "" { + // create color svg dir + err = os.MkdirAll(filepath.Join(path, relnshipName, "icons", "color"), 0777) + if err != nil { + return "", err + } + err = utils.WriteToFile(filepath.Join(path, relnshipName, "icons", "color", relnshipName+"-color.svg"), colorSVG) + if err != nil { + return "", err + } + } + + if whiteSVG != "" { + // create white svg dir + err = os.MkdirAll(filepath.Join(path, relnshipName, "icons", "white"), 0777) + if err != nil { + return "", err + } + err = utils.WriteToFile(filepath.Join(path, relnshipName, "icons", "white", relnshipName+"-white.svg"), whiteSVG) + if err != nil { + return "", err + } + } + } + + return relationshipMetadata, nil +} + +// getSVGForRelationship extracts SVG data from a relationship's styles, falling back to model SVGs if not present +func getSVGForRelationship(model ModelCSV, relationship RelationshipCSV) (colorSVG string, whiteSVG string) { + // Try to extract SVGs from the relationship's Styles JSON + if relationship.Styles != "" { + var styles struct { + SvgColor string `json:"svgColor"` + SvgWhite string `json:"svgWhite"` + } + if err := encoding.Unmarshal([]byte(relationship.Styles), &styles); err == nil { + colorSVG = styles.SvgColor + whiteSVG = styles.SvgWhite + } + } + + // Fall back to model SVGs if relationship doesn't have its own + if colorSVG == "" { + colorSVG = model.SVGColor + } + + if whiteSVG == "" { + whiteSVG = model.SVGWhite + } + return +} + func CreateComponentsMetadataAndCreateSVGsForMDStyle(model ModelCSV, components []ComponentCSV, path, svgDir string) (string, error) { err := os.MkdirAll(filepath.Join(path), 0777) if err != nil { @@ -464,43 +540,42 @@ func getSVGForComponent(model ModelCSV, component ComponentCSV) (colorSVG string return } - func getMinimalUICapabilitiesFromSchema() ([]capability.Capability, error) { - schema, err := schemas.Schemas.ReadFile("schemas/constructs/v1beta1/component/component.json") - if err != nil { - return nil, fmt.Errorf("failed to read component schema: %v", err) - } - - capabilitiesJSON, err := extractCapabilitiesJSONFromSchema(schema) - if err != nil { - return nil, fmt.Errorf("failed to extract capabilities from schema: %v", err) - } - - var allCapabilities []capability.Capability - if err := json.Unmarshal(capabilitiesJSON, &allCapabilities); err != nil { - return nil, fmt.Errorf("failed to unmarshal capabilities: %v", err) - } - - if len(allCapabilities) >= 3 { - return allCapabilities[len(allCapabilities)-3:], nil - } - - return nil, fmt.Errorf("insufficient default capabilities in schema, found %d", len(allCapabilities)) + schema, err := schemas.Schemas.ReadFile("schemas/constructs/v1beta1/component/component.json") + if err != nil { + return nil, fmt.Errorf("failed to read component schema: %v", err) + } + + capabilitiesJSON, err := extractCapabilitiesJSONFromSchema(schema) + if err != nil { + return nil, fmt.Errorf("failed to extract capabilities from schema: %v", err) + } + + var allCapabilities []capability.Capability + if err := json.Unmarshal(capabilitiesJSON, &allCapabilities); err != nil { + return nil, fmt.Errorf("failed to unmarshal capabilities: %v", err) + } + + if len(allCapabilities) >= 3 { + return allCapabilities[len(allCapabilities)-3:], nil + } + + return nil, fmt.Errorf("insufficient default capabilities in schema, found %d", len(allCapabilities)) } func extractCapabilitiesJSONFromSchema(schema []byte) ([]byte, error) { - var schemaMap map[string]interface{} - if err := json.Unmarshal(schema, &schemaMap); err != nil { - return nil, err - } - - if properties, ok := schemaMap["properties"].(map[string]interface{}); ok { - if capabilitiesSchema, ok := properties["capabilities"].(map[string]interface{}); ok { - if defaultValue, ok := capabilitiesSchema["default"]; ok { - return json.Marshal(defaultValue) - } - } - } - - return nil, fmt.Errorf("default capabilities not found in schema") -} \ No newline at end of file + var schemaMap map[string]interface{} + if err := json.Unmarshal(schema, &schemaMap); err != nil { + return nil, err + } + + if properties, ok := schemaMap["properties"].(map[string]interface{}); ok { + if capabilitiesSchema, ok := properties["capabilities"].(map[string]interface{}); ok { + if defaultValue, ok := capabilitiesSchema["default"]; ok { + return json.Marshal(defaultValue) + } + } + } + + return nil, fmt.Errorf("default capabilities not found in schema") +} diff --git a/registry/component_test.go b/registry/component_test.go index 09fb16a4..5ee97ca5 100644 --- a/registry/component_test.go +++ b/registry/component_test.go @@ -1,85 +1,214 @@ package registry import ( - "testing" - "github.com/stretchr/testify/assert" - "github.com/meshery/schemas/models/v1beta1/component" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/meshery/meshkit/utils" + "github.com/meshery/meshkit/utils/manifests" + "github.com/meshery/schemas/models/v1beta1/component" + "github.com/stretchr/testify/assert" ) func TestUpdateCompDefinitionWithDefaultCapabilities(t *testing.T) { - tests := []struct { - name string - csvCapabilities string - expectedCapabilitiesLen int - shouldHaveDefaultCaps bool - }{ - { - name: "Empty capabilities should get defaults", - csvCapabilities: "", - expectedCapabilitiesLen: 3, - shouldHaveDefaultCaps: true, - }, - { - name: "Null capabilities should get defaults", - csvCapabilities: "null", - expectedCapabilitiesLen: 3, - shouldHaveDefaultCaps: true, - }, - { - name: "Existing capabilities should be preserved", - csvCapabilities: `[{"displayName":"Custom Cap","kind":"test"}]`, - expectedCapabilitiesLen: 1, - shouldHaveDefaultCaps: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - comp := ComponentCSV{ - Component: "TestComponent", - Capabilities: tt.csvCapabilities, - Registrant: "meshery", - Model: "test-model", - } - - compDef := &component.ComponentDefinition{} - - err := comp.UpdateCompDefinition(compDef) - - assert.NoError(t, err) - assert.NotNil(t, compDef.Capabilities) - assert.Len(t, *compDef.Capabilities, tt.expectedCapabilitiesLen) - - if tt.shouldHaveDefaultCaps { - capabilities := *compDef.Capabilities - - expectedNames := []string{"Styling", "Change Shape", "Compound Drag And Drop"} - actualNames := make([]string, len(capabilities)) - for i, cap := range capabilities { - actualNames[i] = cap.DisplayName - } - - assert.ElementsMatch(t, expectedNames, actualNames) - - assert.Equal(t, "Styling", capabilities[0].DisplayName) - assert.Equal(t, "mutate", capabilities[0].Kind) - assert.Equal(t, "style", capabilities[0].Type) - } - }) - } + tests := []struct { + name string + csvCapabilities string + expectedCapabilitiesLen int + shouldHaveDefaultCaps bool + }{ + { + name: "Empty capabilities should get defaults", + csvCapabilities: "", + expectedCapabilitiesLen: 3, + shouldHaveDefaultCaps: true, + }, + { + name: "Null capabilities should get defaults", + csvCapabilities: "null", + expectedCapabilitiesLen: 3, + shouldHaveDefaultCaps: true, + }, + { + name: "Existing capabilities should be preserved", + csvCapabilities: `[{"displayName":"Custom Cap","kind":"test"}]`, + expectedCapabilitiesLen: 1, + shouldHaveDefaultCaps: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + comp := ComponentCSV{ + Component: "TestComponent", + Capabilities: tt.csvCapabilities, + Registrant: "meshery", + Model: "test-model", + } + + compDef := &component.ComponentDefinition{} + + err := comp.UpdateCompDefinition(compDef) + + assert.NoError(t, err) + assert.NotNil(t, compDef.Capabilities) + assert.Len(t, *compDef.Capabilities, tt.expectedCapabilitiesLen) + + if tt.shouldHaveDefaultCaps { + capabilities := *compDef.Capabilities + + expectedNames := []string{"Styling", "Change Shape", "Compound Drag And Drop"} + actualNames := make([]string, len(capabilities)) + for i, cap := range capabilities { + actualNames[i] = cap.DisplayName + } + + assert.ElementsMatch(t, expectedNames, actualNames) + + assert.Equal(t, "Styling", capabilities[0].DisplayName) + assert.Equal(t, "mutate", capabilities[0].Kind) + assert.Equal(t, "style", capabilities[0].Type) + } + }) + } } func TestGetMinimalUICapabilitiesFromSchema(t *testing.T) { - capabilities, err := getMinimalUICapabilitiesFromSchema() - - assert.NoError(t, err) - assert.Len(t, capabilities, 3) - - expectedNames := []string{"Styling", "Change Shape", "Compound Drag And Drop"} - actualNames := make([]string, len(capabilities)) - for i, cap := range capabilities { - actualNames[i] = cap.DisplayName - } - - assert.ElementsMatch(t, expectedNames, actualNames) -} \ No newline at end of file + capabilities, err := getMinimalUICapabilitiesFromSchema() + + assert.NoError(t, err) + assert.Len(t, capabilities, 3) + + expectedNames := []string{"Styling", "Change Shape", "Compound Drag And Drop"} + actualNames := make([]string, len(capabilities)) + for i, cap := range capabilities { + actualNames[i] = cap.DisplayName + } + + assert.ElementsMatch(t, expectedNames, actualNames) +} + +func TestGetSVGForRelationship(t *testing.T) { + tests := []struct { + name string + model ModelCSV + relationship RelationshipCSV + expectedColorSVG string + expectedWhiteSVG string + }{ + { + name: "Relationship with its own SVGs", + model: ModelCSV{ + SVGColor: "model-color", + SVGWhite: "model-white", + }, + relationship: RelationshipCSV{ + KIND: "edge", + SubType: "binding", + Styles: `{"svgColor": "rel-color", "svgWhite": "rel-white"}`, + }, + expectedColorSVG: "rel-color", + expectedWhiteSVG: "rel-white", + }, + { + name: "Relationship falls back to model SVGs", + model: ModelCSV{ + SVGColor: "model-color", + SVGWhite: "model-white", + }, + relationship: RelationshipCSV{ + KIND: "edge", + SubType: "binding", + Styles: `{}`, + }, + expectedColorSVG: "model-color", + expectedWhiteSVG: "model-white", + }, + { + name: "Relationship with no styles uses model SVGs", + model: ModelCSV{ + SVGColor: "model-color", + SVGWhite: "model-white", + }, + relationship: RelationshipCSV{ + KIND: "edge", + SubType: "binding", + Styles: "", + }, + expectedColorSVG: "model-color", + expectedWhiteSVG: "model-white", + }, + { + name: "Relationship with partial SVGs", + model: ModelCSV{ + SVGColor: "model-color", + SVGWhite: "model-white", + }, + relationship: RelationshipCSV{ + KIND: "edge", + SubType: "binding", + Styles: `{"svgColor": "rel-color"}`, + }, + expectedColorSVG: "rel-color", + expectedWhiteSVG: "model-white", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + colorSVG, whiteSVG := getSVGForRelationship(tt.model, tt.relationship) + assert.Equal(t, tt.expectedColorSVG, colorSVG) + assert.Equal(t, tt.expectedWhiteSVG, whiteSVG) + }) + } +} + +func TestCreateRelationshipsMetadataAndCreateSVGsForMDStyle(t *testing.T) { + // Create a temporary directory for the test + tmpDir, err := os.MkdirTemp("", "relationship-svg-test-md") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + model := ModelCSV{ + SVGColor: "model-color", + SVGWhite: "model-white", + } + + relationships := []RelationshipCSV{ + { + KIND: "edge", + SubType: "binding", + Type: "hierarchical", + Description: "Test relationship", + Styles: `{"svgColor": "rel-color", "svgWhite": "rel-white"}`, + }, + } + + svgDir := "icons" + metadata, err := CreateRelationshipsMetadataAndCreateSVGsForMDStyle(model, relationships, tmpDir, svgDir) + assert.NoError(t, err) + assert.NotEmpty(t, metadata) + + // Verify metadata structure + assert.Contains(t, metadata, "edge") + assert.Contains(t, metadata, "hierarchical") + assert.Contains(t, metadata, "Test relationship") + + // Verify SVG files were created - derive name the same way as implementation + rel := relationships[0] + relnshipName := utils.FormatName(manifests.FormatToReadableString(fmt.Sprintf("%s-%s", rel.KIND, rel.SubType))) + colorSVGPath := filepath.Join(tmpDir, relnshipName, "icons", "color", relnshipName+"-color.svg") + whiteSVGPath := filepath.Join(tmpDir, relnshipName, "icons", "white", relnshipName+"-white.svg") + + // Check color SVG exists and has correct content + colorContent, err := os.ReadFile(colorSVGPath) + assert.NoError(t, err) + assert.Equal(t, "rel-color", string(colorContent)) + + // Check white SVG exists and has correct content + whiteContent, err := os.ReadFile(whiteSVGPath) + assert.NoError(t, err) + assert.Equal(t, "rel-white", string(whiteContent)) +}