github.com/tilotech/tilores-cli@v0.28.0/cmd/rulessimulate.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  
    11  	"github.com/spf13/cobra"
    12  )
    13  
    14  const tiloTechAPI = "https://api.tilotech.io"
    15  
    16  const colorReset = "\033[0m"
    17  const colorRed = "\033[31m"
    18  const colorGreen = "\033[32m"
    19  const colorYellow = "\033[33m"
    20  
    21  var (
    22  	asJSON bool
    23  )
    24  
    25  // rulesSimulateCmd represents rules simulate command
    26  var rulesSimulateCmd = &cobra.Command{
    27  	Use:   "simulate",
    28  	Short: "Simulates the rules in rule-config.json and tries to match the provided records.",
    29  	Long: `Simulates the rules in rule-config.json and tries to match the provided records.
    30  Reads the rule configuration from "./rule-config.json" file.
    31  Reads both records to match as json from standard input.
    32  
    33  Usage example:
    34  cat records.json | tilores-cli rules simulate
    35  Where records.json contains the following:
    36  {
    37    "recordA": {
    38      "myCustomField": "same value"
    39    },
    40    "recordB": {
    41      "myCustomField": "same value"
    42    }
    43  }
    44  `,
    45  	Run: func(_ *cobra.Command, _ []string) {
    46  		simulateRulesOutput, err := simulateRules()
    47  		cobra.CheckErr(err)
    48  		if asJSON {
    49  			err := json.NewEncoder(os.Stdout).Encode(simulateRulesOutput.TiloRes.SimulateRules)
    50  			cobra.CheckErr(err)
    51  		} else {
    52  			printNicely(simulateRulesOutput)
    53  		}
    54  	},
    55  }
    56  
    57  func printNicely(output *rulesSimulateOutput) {
    58  	for _, searchRuleSet := range output.TiloRes.SimulateRules.SearchRuleSets {
    59  		fmt.Printf("Search Rule Set: %v\n", searchRuleSet.Name)
    60  		printRuleSets(searchRuleSet.RuleSet)
    61  	}
    62  	for _, mutationGroup := range output.TiloRes.SimulateRules.MutationRuleSetGroups {
    63  		fmt.Printf("Mutation Rule Set Group: %v\n", mutationGroup.Name)
    64  		fmt.Println("Link Rule Set:")
    65  		printRuleSets(mutationGroup.LinkRuleSet)
    66  		fmt.Println("Deduplicate Rule Set:")
    67  		printRuleSets(mutationGroup.DeduplicateRuleSet)
    68  	}
    69  }
    70  
    71  func printRuleSets(rs ruleSet) {
    72  	fmt.Printf("Rule Set ID: %v\n", rs.RuleSetID)
    73  	for _, satisfiedRule := range rs.SatisfiedRules {
    74  		fmt.Printf("%v: %ssatisfied%s\n", satisfiedRule, colorGreen, colorReset)
    75  	}
    76  	for _, unsatisfiedRule := range rs.UnsatisfiedRules {
    77  		fmt.Printf("%v: %sunsatisfied%s\n", unsatisfiedRule, colorRed, colorReset)
    78  	}
    79  	fmt.Println()
    80  }
    81  
    82  func init() {
    83  	rulesCmd.AddCommand(rulesSimulateCmd)
    84  
    85  	rulesSimulateCmd.Flags().BoolVarP(&asJSON, "json", "j", false, "Shows output as JSON")
    86  }
    87  
    88  // RulesSimulateInput contains the values that need to be send during a simulate request.
    89  type RulesSimulateInput struct {
    90  	RecordA    map[string]interface{} `json:"recordA"`
    91  	RecordB    map[string]interface{} `json:"recordB"`
    92  	RuleConfig map[string]interface{} `json:"ruleConfig"`
    93  }
    94  
    95  type ruleSet struct {
    96  	RuleSetID        string   `json:"ruleSetID"`
    97  	SatisfiedRules   []string `json:"satisfiedRules"`
    98  	UnsatisfiedRules []string `json:"unsatisfiedRules"`
    99  }
   100  
   101  type responseSearchRuleSet struct {
   102  	Name    string  `json:"name"`
   103  	RuleSet ruleSet `json:"ruleSet"`
   104  }
   105  
   106  type responseMutationRuleSetGroup struct {
   107  	Name               string  `json:"name"`
   108  	LinkRuleSet        ruleSet `json:"linkRuleSet"`
   109  	DeduplicateRuleSet ruleSet `json:"deduplicateRuleSet"`
   110  }
   111  
   112  type rulesSimulateOutput struct {
   113  	TiloRes struct {
   114  		SimulateRules struct {
   115  			SearchRuleSets        []responseSearchRuleSet        `json:"searchRuleSets"`
   116  			MutationRuleSetGroups []responseMutationRuleSetGroup `json:"mutationRuleSetGroups"`
   117  		} `json:"simulateETM"`
   118  	} `json:"tiloRes"`
   119  }
   120  
   121  type gqlResult struct {
   122  	Errors              []interface{}       `json:"errors"`
   123  	SimulateRulesOutput rulesSimulateOutput `json:"data"`
   124  }
   125  
   126  func simulateRules() (*rulesSimulateOutput, error) {
   127  	simulateRulesInput := &RulesSimulateInput{}
   128  	err := json.NewDecoder(os.Stdin).Decode(simulateRulesInput)
   129  	if err != nil {
   130  		return nil, fmt.Errorf("unable to decode input records from standard input: %v", err)
   131  	}
   132  
   133  	ruleConfigFile, err := os.Open("./rule-config.json")
   134  	if err != nil {
   135  		return nil, fmt.Errorf("unable to open rule-config.json: %v", err)
   136  	}
   137  	err = json.NewDecoder(ruleConfigFile).Decode(&simulateRulesInput.RuleConfig)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("unable to decode rule-config.json: %v", err)
   140  	}
   141  
   142  	return callTiloTechAPI(simulateRulesInput)
   143  }
   144  
   145  func callTiloTechAPI(simulateRulesInput *RulesSimulateInput) (*rulesSimulateOutput, error) {
   146  	inputA, err := json.Marshal(simulateRulesInput.RecordA)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	inputB, err := json.Marshal(simulateRulesInput.RecordB)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	ruleConfig, err := json.Marshal(simulateRulesInput.RuleConfig)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	body := struct {
   160  		Query     string      `json:"query"`
   161  		Variables interface{} `json:"variables"`
   162  	}{
   163  		Query: `query simulate($recordA: AWSJSON!, $recordB: AWSJSON!, $ruleConfig: AWSJSON!) {
   164  	tiloRes {
   165  		simulateETM(simulateETMInput: {
   166  				inputA: $recordA
   167  				inputB: $recordB
   168  				etmConfig: $ruleConfig
   169  		}) {
   170  			searchRuleSets{
   171  				name
   172  				ruleSet {
   173  					ruleSetID
   174  					satisfiedRules
   175  					unsatisfiedRules
   176  				}
   177  			}
   178  			mutationRuleSetGroups{
   179  				name
   180  				linkRuleSet{
   181  					ruleSetID
   182  					satisfiedRules
   183  					unsatisfiedRules
   184  				}
   185  				deduplicateRuleSet{
   186  					ruleSetID
   187  					satisfiedRules
   188  					unsatisfiedRules
   189  				}
   190  			}
   191  		}
   192  	}
   193  }
   194  `,
   195  		Variables: map[string]string{
   196  			"recordA":    string(inputA),
   197  			"recordB":    string(inputB),
   198  			"ruleConfig": string(ruleConfig),
   199  		},
   200  	}
   201  
   202  	requestBody, err := json.Marshal(body)
   203  	if err != nil {
   204  		return nil, fmt.Errorf("failed to marshal RulesSimulateInput %s, error was %v\n", requestBody, err) //nolint:revive
   205  	}
   206  
   207  	gqlRes := gqlResult{}
   208  	res, err := http.Post(tiloTechAPI, "application/json", bytes.NewReader(requestBody))
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	if res.StatusCode < 200 || res.StatusCode > 299 {
   214  		return nil, fmt.Errorf("invalid status code %s\n", res.Status) //nolint:revive
   215  	}
   216  	err = unmarshalResponse(res, &gqlRes)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("failed to unmarshal response %v, error was %v\n", res, err) //nolint:revive
   219  	}
   220  	if len(gqlRes.Errors) != 0 {
   221  		return nil, fmt.Errorf("GraphQL errors occured for request %s, errors were %v\n", requestBody, gqlRes.Errors) //nolint:revive
   222  	}
   223  
   224  	return &gqlRes.SimulateRulesOutput, nil
   225  }
   226  
   227  func unmarshalResponse(res *http.Response, v interface{}) error {
   228  	resBody, err := io.ReadAll(res.Body)
   229  	if err != nil {
   230  		return err
   231  	}
   232  
   233  	err = json.Unmarshal(resBody, v)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	return nil
   239  }