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  }