diff --git a/server/internal/adapter/gql/resolver_mutation_project.go b/server/internal/adapter/gql/resolver_mutation_project.go index 1c472954be..6012efd375 100644 --- a/server/internal/adapter/gql/resolver_mutation_project.go +++ b/server/internal/adapter/gql/resolver_mutation_project.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "io" "os" "strings" "time" @@ -14,9 +13,11 @@ import ( "github.com/oklog/ulid" "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" "github.com/reearth/reearth/server/internal/usecase/interfaces" + "github.com/reearth/reearth/server/pkg/file" "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearth/server/pkg/visualizer" "github.com/reearth/reearthx/account/accountdomain" + "github.com/spf13/afero" "golang.org/x/exp/rand" ) @@ -119,12 +120,12 @@ func (r *mutationResolver) DeleteProject(ctx context.Context, input gqlmodel.Del } func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.ExportProjectInput) (*gqlmodel.ExportProjectPayload, error) { + fs := afero.NewOsFs() - // create zip file instance t := time.Now().UTC() entropy := ulid.Monotonic(rand.New(rand.NewSource(uint64(t.UnixNano()))), 0) name := ulid.MustNew(ulid.Timestamp(t), entropy) - zipFile, err := os.Create(fmt.Sprintf("%s.reearth", name.String())) + zipFile, err := fs.Create(fmt.Sprintf("%s.reearth", name.String())) if err != nil { return nil, err } @@ -132,6 +133,7 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp if cerr := zipFile.Close(); cerr != nil && err == nil { err = cerr } + // delete after saving to storage if cerr := os.Remove(zipFile.Name()); cerr != nil && err == nil { err = cerr } @@ -153,14 +155,12 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp return nil, err } - // export scene sce, data, err := usecases(ctx).Scene.ExportScene(ctx, prj, zipWriter) if err != nil { return nil, err } data["project"] = gqlmodel.ToProject(prj) - // export plugins plgs, err := usecases(ctx).Plugin.ExportPlugins(ctx, sce, zipWriter) if err != nil { return nil, err @@ -179,75 +179,45 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.ImportProjectInput) (*gqlmodel.ImportProjectPayload, error) { - fileBytes, err := io.ReadAll(input.File.File) + data, assets, plugins, err := file.UncompressExportZip(input.File.File) if err != nil { return nil, err } - reader, err := zip.NewReader(bytes.NewReader(fileBytes), int64(len(fileBytes))) - if err != nil { - return nil, err - } - - var fileContent []byte + // Assets file import changedFileName := make(map[string]string) + for fileName, file := range assets { + parts1 := strings.Split(fileName, "/") + beforeName := parts1[0] - for _, file := range reader.File { - - if strings.HasPrefix(file.Name, "assets/") { - // Assets file import - - trimmedName := strings.TrimPrefix(file.Name, "assets/") - parts1 := strings.Split(trimmedName, "/") - beforeName := parts1[0] - url, _, err := usecases(ctx).Asset.UploadAssetFile(ctx, beforeName, file) - if err != nil { - return nil, err - } - parts2 := strings.Split(url.Path, "/") - afterName := parts2[len(parts2)-1] - - changedFileName[beforeName] = afterName - - } else if strings.HasPrefix(file.Name, "plugins/") { - // Plugin file import - - trimmedName := strings.TrimPrefix(file.Name, "plugins/") - parts := strings.Split(trimmedName, "/") - pid, err := id.PluginIDFrom(parts[0]) - if err != nil { - return nil, err - } - if err := usecases(ctx).Plugin.ImporPluginFile(ctx, pid, parts[1], file); err != nil { - return nil, err - } - } else if file.Name == "project.json" { - // Data import - - rc, err := file.Open() - if err != nil { - return nil, err - } - defer func(rc io.ReadCloser) { - if cerr := rc.Close(); cerr != nil { - fmt.Printf("Error closing file: %v\n", cerr) - } - }(rc) - fileContent, err = io.ReadAll(rc) - if err != nil { - return nil, err - } - + url, _, err := usecases(ctx).Asset.UploadAssetFile(ctx, beforeName, file) + if err != nil { + return nil, err } + parts2 := strings.Split(url.Path, "/") + afterName := parts2[len(parts2)-1] + changedFileName[beforeName] = afterName + } + + // Plugin file import + for fileName, file := range plugins { + parts := strings.Split(fileName, "/") + pid, err := id.PluginIDFrom(parts[0]) + if err != nil { + return nil, err + } + if err := usecases(ctx).Plugin.ImporPluginFile(ctx, pid, parts[1], file); err != nil { + return nil, err + } } for beforeName, afterName := range changedFileName { - fileContent = bytes.Replace(fileContent, []byte(beforeName), []byte(afterName), -1) + data = bytes.Replace(data, []byte(beforeName), []byte(afterName), -1) } var jsonData map[string]interface{} - if err := json.Unmarshal(fileContent, &jsonData); err != nil { + if err := json.Unmarshal(data, &jsonData); err != nil { return nil, err } @@ -269,7 +239,7 @@ func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.Imp } sceneData, _ := jsonData["scene"].(map[string]interface{}) - sce, err := usecases(ctx).Scene.ImportScene(ctx, prj, sceneData) + sce, err := usecases(ctx).Scene.ImportScene(ctx, prj, plgs, sceneData) if err != nil { return nil, err } diff --git a/server/internal/infrastructure/fs/file.go b/server/internal/infrastructure/fs/file.go index 3c9614cb72..684850b3fa 100644 --- a/server/internal/infrastructure/fs/file.go +++ b/server/internal/infrastructure/fs/file.go @@ -129,13 +129,18 @@ func (f *fileRepo) ReadExportProjectZip(ctx context.Context, filename string) (i return f.read(ctx, filepath.Join(exportDir, sanitize.Path(filename))) } -func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile *os.File) error { - _, err := f.upload(ctx, path.Join(exportDir, zipFile.Name()), zipFile) +func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile afero.File) error { + + file, ok := zipFile.(*os.File) + if !ok { + return errors.New("invalid file type: expected *os.File") + } + _, err := f.upload(ctx, path.Join(exportDir, sanitize.Path(file.Name())), file) return err } func (f *fileRepo) RemoveExportProjectZip(ctx context.Context, filename string) error { - return f.delete(ctx, filepath.Join(exportDir, filename)) + return f.delete(ctx, filepath.Join(exportDir, sanitize.Path(filename))) } // helpers diff --git a/server/internal/infrastructure/gcs/file.go b/server/internal/infrastructure/gcs/file.go index 966cfafbd2..32dbddff97 100644 --- a/server/internal/infrastructure/gcs/file.go +++ b/server/internal/infrastructure/gcs/file.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/url" - "os" "path" "strings" @@ -17,6 +16,7 @@ import ( "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearthx/log" "github.com/reearth/reearthx/rerror" + "github.com/spf13/afero" "google.golang.org/api/iterator" ) @@ -213,7 +213,7 @@ func (f *fileRepo) ReadExportProjectZip(ctx context.Context, name string) (io.Re return f.read(ctx, path.Join(gcsExportBasePath, sn)) } -func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile *os.File) error { +func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile afero.File) error { _, err := f.upload(ctx, path.Join(gcsExportBasePath, zipFile.Name()), zipFile) return err } diff --git a/server/internal/infrastructure/s3/s3.go b/server/internal/infrastructure/s3/s3.go index deb8e21945..d3429e74e6 100644 --- a/server/internal/infrastructure/s3/s3.go +++ b/server/internal/infrastructure/s3/s3.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/url" - "os" "path" "strings" @@ -22,6 +21,7 @@ import ( "github.com/reearth/reearthx/log" "github.com/reearth/reearthx/rerror" "github.com/samber/lo" + "github.com/spf13/afero" ) const ( @@ -222,13 +222,15 @@ func (f *fileRepo) ReadExportProjectZip(ctx context.Context, name string) (io.Re return f.read(ctx, path.Join(exportBasePath, sn)) } -func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile *os.File) error { - _, err := f.upload(ctx, path.Join(exportBasePath, zipFile.Name()), zipFile) +func (f *fileRepo) UploadExportProjectZip(ctx context.Context, zipFile afero.File) error { + sanitizedName := sanitize.Path(zipFile.Name()) + _, err := f.upload(ctx, path.Join(exportBasePath, sanitizedName), zipFile) return err } func (f *fileRepo) RemoveExportProjectZip(ctx context.Context, filename string) error { - return f.delete(ctx, path.Join(exportBasePath, filename)) + sanitizedFilename := sanitize.Path(filename) + return f.delete(ctx, path.Join(exportBasePath, sanitizedFilename)) } // helpers diff --git a/server/internal/usecase/gateway/file.go b/server/internal/usecase/gateway/file.go index cf7e6e9393..f0bd4b77c0 100644 --- a/server/internal/usecase/gateway/file.go +++ b/server/internal/usecase/gateway/file.go @@ -5,10 +5,10 @@ import ( "errors" "io" "net/url" - "os" "github.com/reearth/reearth/server/pkg/file" "github.com/reearth/reearth/server/pkg/id" + "github.com/spf13/afero" ) var ( @@ -38,6 +38,6 @@ type File interface { RemoveStory(context.Context, string) error ReadExportProjectZip(context.Context, string) (io.ReadCloser, error) - UploadExportProjectZip(context.Context, *os.File) error + UploadExportProjectZip(context.Context, afero.File) error RemoveExportProjectZip(context.Context, string) error } diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go index 44e04132f8..23743bcc0a 100644 --- a/server/internal/usecase/interactor/project.go +++ b/server/internal/usecase/interactor/project.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "os" "strings" "time" @@ -26,6 +25,7 @@ import ( "github.com/reearth/reearthx/account/accountusecase/accountrepo" "github.com/reearth/reearthx/rerror" "github.com/reearth/reearthx/usecasex" + "github.com/spf13/afero" ) type Project struct { @@ -511,6 +511,11 @@ func (i *Project) ExportProject(ctx context.Context, projectID id.ProjectID, zip if err != nil { return nil, err } + defer func() { + if cerr := stream.Close(); cerr != nil { + fmt.Printf("Error closing file: %v\n", cerr) + } + }() zipEntryPath := fmt.Sprintf("assets/%s", trimmedName) zipEntry, err := zipWriter.Create(zipEntryPath) if err != nil { @@ -521,15 +526,12 @@ func (i *Project) ExportProject(ctx context.Context, projectID id.ProjectID, zip _ = stream.Close() return nil, err } - if err := stream.Close(); err != nil { - return nil, err - } } return prj, nil } -func (i *Project) UploadExportProjectZip(ctx context.Context, zipWriter *zip.Writer, zipFile *os.File, data map[string]interface{}, prj *project.Project) error { +func (i *Project) UploadExportProjectZip(ctx context.Context, zipWriter *zip.Writer, zipFile afero.File, data map[string]interface{}, prj *project.Project) error { fileWriter, err := zipWriter.Create("project.json") if err != nil { return err @@ -546,15 +548,14 @@ func (i *Project) UploadExportProjectZip(ctx context.Context, zipWriter *zip.Wri return err } - // flush once - if err := zipFile.Close(); err != nil { + if _, err := zipFile.Seek(0, 0); err != nil { return err } - zipFile, err = os.Open(zipFile.Name()) - if err != nil { - return err - } - + defer func() { + if err := zipFile.Close(); err != nil { + fmt.Println("Failed to close zip file:", err) + } + }() if err := i.file.UploadExportProjectZip(ctx, zipFile); err != nil { return err } diff --git a/server/internal/usecase/interactor/scene.go b/server/internal/usecase/interactor/scene.go index 93ddf92367..e6a4c420f3 100644 --- a/server/internal/usecase/interactor/scene.go +++ b/server/internal/usecase/interactor/scene.go @@ -666,7 +666,13 @@ func (i *Scene) ExportScene(ctx context.Context, prj *project.Project, zipWriter // widget button icon for _, property := range widgetProperties { for _, item := range property.Items() { + if item == nil { + continue + } for _, field := range item.Fields(nil) { + if field == nil || field.Value() == nil || field.Value().Value() == nil { + continue + } if field.GuessSchema().ID().String() == "buttonIcon" { u, ok := field.Value().Value().(*url.URL) if !ok { @@ -715,7 +721,7 @@ func (i *Scene) ExportScene(ctx context.Context, prj *project.Project, zipWriter return sce, res, nil } -func (i *Scene) ImportScene(ctx context.Context, prj *project.Project, sceneData map[string]interface{}) (*scene.Scene, error) { +func (i *Scene) ImportScene(ctx context.Context, prj *project.Project, plgs []*plugin.Plugin, sceneData map[string]interface{}) (*scene.Scene, error) { sceneJSON, err := builder.ParseSceneJSON(ctx, sceneData) if err != nil { return nil, err @@ -724,6 +730,16 @@ func (i *Scene) ImportScene(ctx context.Context, prj *project.Project, sceneData if err != nil { return nil, err } + + plugins := scene.NewPlugins([]*scene.Plugin{ + scene.NewPlugin(id.OfficialPluginID, nil), + }) + for _, plg := range plgs { + if plg.ID().String() != "reearth" { + plugins.Add(scene.NewPlugin(plg.ID(), nil)) + } + } + widgets := []*scene.Widget{} for _, widgetJSON := range sceneJSON.Widgets { widgetID, err := id.WidgetIDFrom(widgetJSON.ID) @@ -790,9 +806,10 @@ func (i *Scene) ImportScene(ctx context.Context, prj *project.Project, sceneData if err != nil { return nil, err } - tiles := id.PropertySchemaGroupID("tiles") - g := prop.GetOrCreateGroupList(schema, property.PointItemBySchema(tiles)) - g.Add(property.NewGroup().NewID().SchemaGroup(tiles).MustBuild(), -1) + prop, err = builder.AddItemFromPropertyJSON(prop, schema, sceneJSON.Property) + if err != nil { + return nil, err + } rootLayer, err := layer.NewGroup().NewID().Scene(sceneID).Root(true).Build() if err != nil { return nil, err @@ -812,6 +829,7 @@ func (i *Scene) ImportScene(ctx context.Context, prj *project.Project, sceneData UpdatedAt(time.Now()). Property(prop.ID()). Clusters(clusterList). + Plugins(plugins). Build() if err != nil { return nil, err @@ -857,7 +875,13 @@ func (i *Scene) getWidgePlugin(ctx context.Context, pid id.PluginID, eid id.Plug func (i *Scene) addZipAsset(ctx context.Context, zipWriter *zip.Writer, url string) error { parts := strings.Split(url, "/") + if len(parts) == 0 { + return errors.New("invalid URL format") + } fileName := parts[len(parts)-1] + if fileName == "" { + return errors.New("empty filename extracted from URL") + } stream, err := i.file.ReadAsset(ctx, fileName) if err != nil { diff --git a/server/internal/usecase/interactor/scene_test.go b/server/internal/usecase/interactor/scene_test.go index 7bb8fee4aa..31eb8ee993 100644 --- a/server/internal/usecase/interactor/scene_test.go +++ b/server/internal/usecase/interactor/scene_test.go @@ -10,6 +10,7 @@ import ( "github.com/reearth/reearth/server/internal/infrastructure/fs" "github.com/reearth/reearth/server/internal/infrastructure/memory" "github.com/reearth/reearth/server/internal/usecase/gateway" + "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/policy" "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearthx/account/accountdomain/workspace" @@ -140,7 +141,7 @@ func TestImportScene(t *testing.T) { assert.NoError(t, err) // invoke the target function - result, err := ifs.ImportScene(ctx, prj, sceneData) + result, err := ifs.ImportScene(ctx, prj, []*plugin.Plugin{}, sceneData) assert.NoError(t, err) assert.NotNil(t, result) @@ -176,7 +177,11 @@ func TestImportScene(t *testing.T) { "datasetSchemas": null, "id": "01j7g9ddv4sbf8tgt5c6xxj5xc", "newLayers": null, - "plugins": [], + "plugins": [ + { + "pluginId": "reearth" + } + ], "projectId": "%s", "stories": null, "styles": null, diff --git a/server/internal/usecase/interactor/style.go b/server/internal/usecase/interactor/style.go index a1fc9c92d7..433e90779d 100644 --- a/server/internal/usecase/interactor/style.go +++ b/server/internal/usecase/interactor/style.go @@ -236,7 +236,9 @@ func (i *Style) ImportStyles(ctx context.Context, sceneData map[string]interface if err := i.styleRepo.SaveAll(ctx, styleList); err != nil { return nil, err } - + if len(styleIDs) == 0 { + return nil, nil + } styles2, err := i.styleRepo.FindByIDs(ctx, styleIDs) if err != nil { return nil, err diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go index 42c7d48de2..069e132846 100644 --- a/server/internal/usecase/interfaces/project.go +++ b/server/internal/usecase/interfaces/project.go @@ -5,7 +5,6 @@ import ( "context" "errors" "net/url" - "os" "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/pkg/id" @@ -13,6 +12,7 @@ import ( "github.com/reearth/reearth/server/pkg/visualizer" "github.com/reearth/reearthx/account/accountdomain" "github.com/reearth/reearthx/usecasex" + "github.com/spf13/afero" ) type CreateProjectParam struct { @@ -70,5 +70,5 @@ type Project interface { Delete(context.Context, id.ProjectID, *usecase.Operator) error ExportProject(context.Context, id.ProjectID, *zip.Writer, *usecase.Operator) (*project.Project, error) ImportProject(context.Context, map[string]interface{}) (*project.Project, usecasex.Tx, error) - UploadExportProjectZip(context.Context, *zip.Writer, *os.File, map[string]interface{}, *project.Project) error + UploadExportProjectZip(context.Context, *zip.Writer, afero.File, map[string]interface{}, *project.Project) error } diff --git a/server/internal/usecase/interfaces/scene.go b/server/internal/usecase/interfaces/scene.go index cfcd52b5b0..79da387f33 100644 --- a/server/internal/usecase/interfaces/scene.go +++ b/server/internal/usecase/interfaces/scene.go @@ -7,6 +7,7 @@ import ( "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearth/server/pkg/scene" ) @@ -33,7 +34,7 @@ type Scene interface { UpdateCluster(context.Context, UpdateClusterParam, *usecase.Operator) (*scene.Scene, *scene.Cluster, error) RemoveCluster(context.Context, id.SceneID, id.ClusterID, *usecase.Operator) (*scene.Scene, error) ExportScene(context.Context, *project.Project, *zip.Writer) (*scene.Scene, map[string]interface{}, error) - ImportScene(context.Context, *project.Project, map[string]interface{}) (*scene.Scene, error) + ImportScene(context.Context, *project.Project, []*plugin.Plugin, map[string]interface{}) (*scene.Scene, error) } type UpdateWidgetParam struct { diff --git a/server/pkg/file/zip.go b/server/pkg/file/zip.go index 8a6a71b6c9..60c86775ca 100644 --- a/server/pkg/file/zip.go +++ b/server/pkg/file/zip.go @@ -3,6 +3,7 @@ package file import ( "archive/zip" "bytes" + "fmt" "io" "strings" ) @@ -85,3 +86,46 @@ func ZipBasePath(zr *zip.Reader) (b string) { } return } + +func UncompressExportZip(file io.ReadSeeker) ([]byte, map[string]*zip.File, map[string]*zip.File, error) { + fileBytes, err := io.ReadAll(file) + if err != nil { + return nil, nil, nil, err + } + reader, err := zip.NewReader(bytes.NewReader(fileBytes), int64(len(fileBytes))) + if err != nil { + return nil, nil, nil, err + } + var data []byte + assets := make(map[string]*zip.File) + plugins := make(map[string]*zip.File) + for _, file := range reader.File { + if file.Name == "project.json" { + rc, err := file.Open() + if err != nil { + return nil, nil, nil, err + } + defer func(rc io.ReadCloser) { + if cerr := rc.Close(); cerr != nil { + fmt.Printf("Error closing file: %v\n", cerr) + } + }(rc) + data, err = io.ReadAll(rc) + if err != nil { + return nil, nil, nil, err + } + } else if strings.HasPrefix(file.Name, "assets/") { + trimmedName := strings.TrimPrefix(file.Name, "assets/") + assets[trimmedName] = file + } else if strings.HasPrefix(file.Name, "plugins/") { + trimmedName := strings.TrimPrefix(file.Name, "plugins/") + plugins[trimmedName] = file + } else { + return nil, nil, nil, fmt.Errorf("invalid file in zip: %s", file.Name) + } + } + if len(data) == 0 { + return nil, nil, nil, fmt.Errorf("project.json not found in the zip file") + } + return data, assets, plugins, nil +} diff --git a/server/pkg/scene/builder/decoder.go b/server/pkg/scene/builder/decoder.go index d6adb61efc..72cfe4d2ac 100644 --- a/server/pkg/scene/builder/decoder.go +++ b/server/pkg/scene/builder/decoder.go @@ -3,6 +3,7 @@ package builder import ( "context" "encoding/json" + "fmt" "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" "github.com/reearth/reearth/server/pkg/id" @@ -109,20 +110,55 @@ func parseWidgetAreaPadding(paddingJSON *widgetAreaPaddingJSON) *scene.WidgetAre } func AddItemFromPropertyJSON(prop *property.Property, ps *property.Schema, pj propertyJSON) (*property.Property, error) { - for sgKey, value1 := range pj { - schemaGroupID := id.PropertySchemaGroupIDFromRef(&sgKey) - if iVal, ok := value1.(map[string]interface{}); ok { - for fKey, value2 := range iVal { - fieldID := id.PropertyFieldIDFromRef(&fKey) - ptr := property.NewPointer(schemaGroupID, nil, fieldID) - v, ok := parsePropertyValue(value2) - if ok { - _, _, _, err := prop.UpdateValue(ps, ptr, v) + for sgKey, value := range pj { + + if items, ok := value.(map[string]interface{}); ok { + // simple property + + sgID := id.PropertySchemaGroupIDFromRef(&sgKey) + + for fieldKey, value := range items { + + fieldID := id.PropertyFieldIDFromRef(&fieldKey) + ptr := property.NewPointer(sgID, nil, fieldID) + pv, ok := parsePropertyValue(value) + + if ok && ps != nil { + _, _, _, err := prop.UpdateValue(ps, ptr, pv) if err != nil { return nil, err } } } + + } else if arrayProperty, ok := value.([]interface{}); ok { + // group property + + for _, groupProperty := range arrayProperty { + + sg := id.PropertySchemaGroupID(sgKey) + gl := prop.GetOrCreateGroupList(ps, property.PointItemBySchema(sg)) + g := property.NewGroup().NewID().SchemaGroup(sg).MustBuild() + gl.Add(g, -1) + + if items, ok := groupProperty.(map[string]interface{}); ok { + + for fieldKey, value := range items { + if fieldKey == "id" { + continue + } + ov, ok := parsePropertyOptionalValue(value) + if ok { + fieldID := id.PropertyFieldIDFromRef(&fieldKey) + field := property.NewField(*fieldID). + Value(ov). + // Links(flinks). + Build() + g.AddFields(field) + } + } + } + } } } return prop, nil @@ -136,5 +172,15 @@ func parsePropertyValue(value interface{}) (*property.Value, bool) { return property.ValueType(fieldType).ValueFrom(fieldVal), ok } } + fmt.Printf("property is unreadable %v\n", value) + return nil, false +} + +func parsePropertyOptionalValue(value interface{}) (*property.OptionalValue, bool) { + pv, ok := parsePropertyValue(value) + if ok { + ov := property.NewOptionalValue(pv.Type(), pv) + return ov, true + } return nil, false }