github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/buildexpression/merge/merge.go (about)

     1  package merge
     2  
     3  import (
     4  	"encoding/json"
     5  	"reflect"
     6  
     7  	"github.com/ActiveState/cli/internal/errs"
     8  	"github.com/ActiveState/cli/internal/logging"
     9  	"github.com/ActiveState/cli/internal/multilog"
    10  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
    11  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_models"
    12  	"github.com/ActiveState/cli/pkg/platform/runtime/buildexpression"
    13  )
    14  
    15  func Merge(exprA *buildexpression.BuildExpression, exprB *buildexpression.BuildExpression, strategies *mono_models.MergeStrategies) (*buildexpression.BuildExpression, error) {
    16  	if !isAutoMergePossible(exprA, exprB) {
    17  		return nil, errs.New("Unable to merge buildexpressions")
    18  	}
    19  	if len(strategies.Conflicts) > 0 {
    20  		return nil, errs.New("Unable to merge buildexpressions due to conflicting requirements")
    21  	}
    22  
    23  	// Update build expression requirements with merge results.
    24  	for _, req := range strategies.OverwriteChanges {
    25  		var op types.Operation
    26  		err := op.Unmarshal(req.Operation)
    27  		if err != nil {
    28  			return nil, errs.Wrap(err, "Unable to convert requirement operation to buildplan operation")
    29  		}
    30  
    31  		var versionRequirements []types.VersionRequirement
    32  		for _, constraint := range req.VersionConstraints {
    33  			data, err := constraint.MarshalBinary()
    34  			if err != nil {
    35  				return nil, errs.Wrap(err, "Could not marshal requirement version constraints")
    36  			}
    37  			m := make(map[string]string)
    38  			err = json.Unmarshal(data, &m)
    39  			if err != nil {
    40  				return nil, errs.Wrap(err, "Could not unmarshal requirement version constraints")
    41  			}
    42  			versionRequirements = append(versionRequirements, m)
    43  		}
    44  
    45  		bpReq := types.Requirement{
    46  			Name:               req.Requirement,
    47  			Namespace:          req.Namespace,
    48  			VersionRequirement: versionRequirements,
    49  		}
    50  
    51  		if err := exprB.UpdateRequirement(op, bpReq); err != nil {
    52  			return nil, errs.Wrap(err, "Unable to update buildexpression with merge results")
    53  		}
    54  	}
    55  
    56  	return exprB, nil
    57  }
    58  
    59  // isAutoMergePossible determines whether or not it is possible to auto-merge the given build
    60  // expressions.
    61  // This is only possible if the two build expressions differ ONLY in requirements.
    62  func isAutoMergePossible(exprA *buildexpression.BuildExpression, exprB *buildexpression.BuildExpression) bool {
    63  	jsonA, err := getComparableJson(exprA)
    64  	if err != nil {
    65  		multilog.Error("Unable to get buildexpression minus requirements: %v", errs.JoinMessage(err))
    66  		return false
    67  	}
    68  	jsonB, err := getComparableJson(exprB)
    69  	if err != nil {
    70  		multilog.Error("Unable to get buildxpression minus requirements: %v", errs.JoinMessage(err))
    71  		return false
    72  	}
    73  	logging.Debug("Checking for possibility of auto-merging build expressions")
    74  	logging.Debug("JsonA: %v", jsonA)
    75  	logging.Debug("JsonB: %v", jsonB)
    76  	return reflect.DeepEqual(jsonA, jsonB)
    77  }
    78  
    79  // getComparableJson returns a comparable JSON map[string]interface{} structure for the given build
    80  // expression. The map will not have a "requirements" field, nor will it have an "at_time" field.
    81  // String lists will also be sorted.
    82  func getComparableJson(expr *buildexpression.BuildExpression) (map[string]interface{}, error) {
    83  	data, err := json.Marshal(expr)
    84  	if err != nil {
    85  		return nil, errs.New("Unable to unmarshal marshaled buildxpression")
    86  	}
    87  
    88  	m := make(map[string]interface{})
    89  	err = json.Unmarshal(data, &m)
    90  	if err != nil {
    91  		return nil, errs.New("Unable to unmarshal marshaled buildxpression")
    92  	}
    93  
    94  	letValue, ok := m["let"]
    95  	if !ok {
    96  		return nil, errs.New("Build expression has no 'let' key")
    97  	}
    98  	letMap, ok := letValue.(map[string]interface{})
    99  	if !ok {
   100  		return nil, errs.New("'let' key is not a JSON object")
   101  	}
   102  	deleteKey(&letMap, "requirements")
   103  	deleteKey(&letMap, "at_time")
   104  
   105  	return m, nil
   106  }
   107  
   108  // deleteKey recursively iterates over the given JSON map until it finds the given key and deletes
   109  // it and its value.
   110  func deleteKey(m *map[string]interface{}, key string) bool {
   111  	for k, v := range *m {
   112  		if k == key {
   113  			delete(*m, k)
   114  			return true
   115  		}
   116  		if m2, ok := v.(map[string]interface{}); ok {
   117  			if deleteKey(&m2, key) {
   118  				return true
   119  			}
   120  		}
   121  	}
   122  	return false
   123  }