github.com/alkemics/goflow@v0.2.1/gfutil/gfgo/json.go (about) 1 package gfgo 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "os" 8 "sort" 9 "strconv" 10 "strings" 11 12 "gopkg.in/yaml.v2" 13 14 "github.com/alkemics/goflow" 15 ) 16 17 type jsonNode struct { 18 ID string `json:"id"` 19 Pkg string `json:"pkg"` 20 Type string `json:"type"` 21 Inputs []goflow.Field `json:"inputs"` 22 Outputs []goflow.Field `json:"outputs"` 23 Dependencies []goflow.Field `json:"dependencies"` 24 } 25 26 type jsonEdge struct { 27 SourceID string `json:"sourceId"` 28 TargetID string `json:"targetId"` 29 Type string `json:"inputType"` // json tag to have it work with the current dashboard 30 } 31 32 type bindings []string 33 34 func (b *bindings) UnmarshalYAML(unmarshal func(interface{}) error) error { 35 var multipleBindings []string 36 if err := unmarshal(&multipleBindings); err == nil { 37 *b = multipleBindings 38 return nil 39 } 40 41 var simpleBinding string 42 if err := unmarshal(&simpleBinding); err != nil { 43 return err 44 } 45 46 *b = []string{simpleBinding} 47 return nil 48 } 49 50 // WithJSONMarshal wraps the generate function to add a MarshalJSON function 51 // to the graph. 52 // 53 // TODO: 54 // This method does some work similar to the bind, if and after wrappers, there 55 // probably is a simple way to handle that without actually redoing the work. 56 // I tried coalescing the nodes from the bind wrapper, it did not go too well :p 57 func WithJSONMarshal( 58 generate GenerateFunc, 59 yamlFilename string, 60 nodes []Node, 61 ) GenerateFunc { 62 file, err := os.Open(yamlFilename) 63 if err != nil { 64 return func(w io.Writer, g goflow.GraphRenderer) error { 65 return err 66 } 67 } 68 69 var graph struct { 70 Name string 71 Nodes []struct { 72 ID string 73 Type string 74 Bind map[string]bindings 75 If []string 76 After []string 77 } 78 Outputs map[string]bindings 79 } 80 if err := yaml.NewDecoder(file).Decode(&graph); err != nil { 81 return func(w io.Writer, g goflow.GraphRenderer) error { 82 return goflow.ParseYAMLError(err) 83 } 84 } 85 86 jsonNodes := make([]jsonNode, len(graph.Nodes)) 87 edges := make([]jsonEdge, 0) 88 for i, n := range graph.Nodes { 89 jn := jsonNode{ 90 ID: n.ID, 91 } 92 93 for _, node := range nodes { 94 if node.Match(n.Type) { 95 jn.Type = node.Typ 96 jn.Pkg = node.Pkg 97 jn.Inputs = node.Inputs 98 jn.Outputs = node.Outputs 99 jn.Dependencies = node.Dependencies 100 break 101 } 102 } 103 104 jsonNodes[i] = jn 105 106 for inputName, bds := range n.Bind { 107 var typ string 108 for _, input := range jn.Inputs { 109 if inputName == input.Name { 110 typ = input.Type 111 } 112 } 113 114 for _, b := range bds { 115 var sourceID string 116 if split := strings.SplitN(b, ".", 2); len(split) == 2 { 117 sID := split[0] 118 if sID == "inputs" { 119 sourceID = sID 120 } else { 121 for _, on := range graph.Nodes { 122 if on.ID == sID { 123 sourceID = sID 124 } 125 } 126 } 127 } 128 129 if sourceID == "" { 130 sourceID = "manualInput" 131 } 132 133 edges = append(edges, jsonEdge{ 134 SourceID: sourceID, 135 TargetID: n.ID, 136 Type: typ, 137 }) 138 } 139 } 140 141 for _, i := range n.If { 142 i = strings.TrimPrefix(i, "not") 143 i = strings.TrimSpace(i) 144 var sourceID string 145 if split := strings.SplitN(i, ".", 2); len(split) == 2 { 146 sID := split[0] 147 if sID == "inputs" { 148 sourceID = sID 149 } else { 150 for _, on := range graph.Nodes { 151 if on.ID == sID { 152 sourceID = sID 153 } 154 } 155 } 156 } 157 158 if sourceID == "" { 159 sourceID = "manualInput" 160 } 161 162 edges = append(edges, jsonEdge{ 163 SourceID: sourceID, 164 TargetID: n.ID, 165 Type: "bool", 166 }) 167 } 168 169 for _, a := range n.After { 170 edges = append(edges, jsonEdge{ 171 SourceID: a, 172 TargetID: n.ID, 173 Type: "__wait__", 174 }) 175 } 176 } 177 178 for _, bds := range graph.Outputs { 179 for _, b := range bds { 180 var sourceID string 181 if split := strings.SplitN(b, ".", 2); len(split) == 2 { 182 sID := split[0] 183 if sID == "inputs" { 184 sourceID = sID 185 } else { 186 for _, on := range graph.Nodes { 187 if on.ID == sID { 188 sourceID = sID 189 } 190 } 191 } 192 } 193 194 if sourceID == "" { 195 sourceID = "manualInput" 196 } 197 198 edges = append(edges, jsonEdge{ 199 SourceID: sourceID, 200 TargetID: "outputs", 201 Type: "whatever", 202 }) 203 } 204 } 205 206 // Check all the nodes that have not been used 207 for _, n := range jsonNodes { 208 used := false 209 for _, e := range edges { 210 if n.ID == e.SourceID { 211 used = true 212 break 213 } 214 } 215 216 if !used { 217 edges = append(edges, jsonEdge{ 218 SourceID: n.ID, 219 TargetID: "outputs", 220 Type: "__wait__", 221 }) 222 } 223 } 224 225 sort.SliceStable(jsonNodes, func(i, j int) bool { 226 return jsonNodes[i].ID <= jsonNodes[j].ID 227 }) 228 sort.SliceStable(edges, func(i, j int) bool { 229 return fmt.Sprintf("%v", edges[i]) <= fmt.Sprintf("%v", edges[j]) 230 }) 231 232 return func(w io.Writer, graph goflow.GraphRenderer) error { 233 if err := generate(w, graph); err != nil { 234 return err 235 } 236 237 msg, err := json.Marshal(map[string]interface{}{ 238 "id": graph.Name(), 239 "type": graph.Name(), 240 "pkg": graph.Pkg(), 241 242 "filename": yamlFilename, 243 "doc": graph.Doc(), 244 245 "nodes": jsonNodes, 246 "edges": edges, 247 248 "inputs": graph.Inputs(), 249 "outputs": graph.Outputs(), 250 }) 251 if err != nil { 252 return err 253 } 254 255 _, err = fmt.Fprintf(w, ` 256 // MarshalJSON returns the json representation of the graphs. It is pre-generated by 257 // WithJSONMarshal, and hence never returns an error. 258 func (g %s) MarshalJSON() ([]byte, error) { 259 return []byte(%s), nil 260 } 261 `, 262 graph.Name(), 263 strconv.Quote(string(msg)), 264 ) 265 return err 266 } 267 }