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 }