<commit_before>package atc

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
)

// Step is an "envelope" type, acting as a wrapper to handle the marshaling and
// unmarshaling of an underlying StepConfig.
type Step struct {
	Config        StepConfig
	UnknownFields map[string]*json.RawMessage
}

// ErrNoStepConfigured is returned when a step does not have any keys that
// indicate its step type.
var ErrNoStepConfigured = errors.New("no step configured")
var ErrNoCoreStepDeclared = errors.New("no core step type declared (e.g. get, put, task, etc.)")

// UnmarshalJSON unmarshals step configuration in multiple passes, determining
// precedence by the order of StepDetectors listed in the StepPrecedence
// variable.
//
// First, the step data is unmarshalled into a map[string]*json.RawMessage. Next,
// UnmarshalJSON loops over StepPrecedence to determine the type of step.
//
// For any StepDetector with a .Key field present in the map, .New is called to
// construct an empty StepConfig, and then json.Unmarshal is called on it to parse
// the data.
//
// For step modifiers like `timeout:` and `attempts:` they eventuallly wrap a
// core step type (e.g. get, put, task etc.). Core step types do not wrap other
// steps.
//
// When a core step type is encountered parsing stops and any remaining keys in
// rawStepConfig are considered invalid. This is how we stop someone from
// putting a `get` and `put` in the same step while still allowing valid step
// modifiers. This is also why step modifiers are listed first in
// StepPrecedence.
//
// If no StepDetectors match, no step is parsed, ErrNoStepConfigured is
// returned.
func (step *Step) UnmarshalJSON(data []byte) error {
	var rawStepConfig map[string]*json.RawMessage
	err := json.Unmarshal(data, &rawStepConfig)
	if err != nil {
		return err
	}

	var prevStep StepWrapper
	var coreStepDeclared bool
	for _, s := range StepPrecedence {
		_, found := rawStepConfig[s.Key]
		if !found {
			continue
		}

		curStep := s.New()

		err := json.Unmarshal(data, curStep)
		if err != nil {
			return MalformedStepError{
				StepType: s.Key,
				Err:      err,
			}
		}

		if step.Config == nil {
			step.Config = curStep
		}

		if prevStep != nil {
			prevStep.Wrap(curStep)
		}

		deleteKnownFields(rawStepConfig, curStep)

		if wrapper, isWrapper := curStep.(StepWrapper); isWrapper {
			prevStep = wrapper
		} else {
			coreStepDeclared = true
			break
		}

		data, err = json.Marshal(rawStepConfig)
		if err != nil {
			return fmt.Errorf("re-marshal rawStepConfig parsing: %w", err)
		}
	}

	if step.Config == nil {
		return ErrNoStepConfigured
	}

	if !coreStepDeclared {
		return ErrNoCoreStepDeclared
	}

	if len(rawStepConfig) != 0 {
		step.UnknownFields = rawStepConfig
	}

	return nil
}

// MarshalJSON marshals step configuration in multiple passes, looping and
// calling .Unwrap to marshal all nested steps into one big set of fields which
// is then marshalled and returned.
func (step Step) MarshalJSON() ([]byte, error) {
	fields := step.UnknownFields

	unwrapped := step.Config
	for unwrapped != nil {
		payload, err := json.Marshal(unwrapped)
		if err != nil {
			return nil, err
		}

		err = json.Unmarshal(payload, &fields)
		if err != nil {
			return nil, err
		}

		if wrapper, isWrapper := unwrapped.(StepWrapper); isWrapper {
			unwrapped = wrapper.Unwrap()
		} else {
			break
		}
	}

	return json.Marshal(fields)
}

// See the note about json tags here: https://golang.org/pkg/encoding/json/#Marshal
func deleteKnownFields(rawStepConfig map[string]*json.RawMessage, step StepConfig) {
	stepType := reflect.TypeOf(step).Elem()
	for i := 0; i < stepType.NumField(); i++ {
		field := stepType.Field(i)
		jsonTag := field.Tag.Get("json")
		if jsonTag == "-" {
			continue
		}
		jsonTagParts := strings.Split(jsonTag, ",")
		if len(jsonTagParts) < 1 {
			continue
		}
		jsonKey := jsonTagParts[0]
		if jsonKey == "" {
			jsonKey = field.Name
		}
		delete(rawStepConfig, jsonKey)
	}
}

// StepConfig is implemented by all step types.
type StepConfig interface {
	// Visit must call StepVisitor with the appropriate method corresponding to
	// this step type.
	//
	// When a new step type is added, the StepVisitor interface must be extended.
	// This allows the compiler to help us track down all the places where steps
	// must be handled type-by-type.
	Visit(StepVisitor) error
}

// StepWrapper is an optional interface for step types that is implemented by
// steps that wrap/modify other steps (e.g. hooks like `on_success`, `timeout`, etc.)
type StepWrapper interface {
	// Wrap is called during (Step).UnmarshalJSON whenever an 'inner' step is
	// parsed.
	//
	// Modifier step types should implement this by assigning a `json:"-"` tag to
	// an internal field of type StepConfig in their struct.
	Wrap(StepConfig)

	// Unwrap is called during (Step).MarshalJSON and must return the wrapped
	// StepConfig.
	Unwrap() StepConfig
}

// StepVisitor is an interface used to assist in finding all the places that
// need to be updated whenever a new step type is introduced.
//
// Each StepConfig must implement .Visit to call the appropriate method on the
// given StepVisitor.
type StepVisitor interface {
	VisitTask(*TaskStep) error
	VisitGet(*GetStep) error
	VisitPut(*PutStep) error
	VisitSetPipeline(*SetPipelineStep) error
	VisitLoadVar(*LoadVarStep) error
	VisitTry(*TryStep) error
	VisitDo(*DoStep) error
	VisitInParallel(*InParallelStep) error
	VisitAggregate(*AggregateStep) error
	VisitTimeout(*TimeoutStep) error
	VisitRetry(*RetryStep) error
	VisitOnSuccess(*OnSuccessStep) error
	VisitOnFailure(*OnFailureStep) error
	VisitOnAbort(*OnAbortStep) error
	VisitOnError(*OnErrorStep) error<commit_msg>atc/steps: update comment about StepWrapper's<commit_after>package atc

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
)

// Step is an "envelope" type, acting as a wrapper to handle the marshaling and
// unmarshaling of an underlying StepConfig.
type Step struct {
	Config        StepConfig
	UnknownFields map[string]*json.RawMessage
}

// ErrNoStepConfigured is returned when a step does not have any keys that
// indicate its step type.
var ErrNoStepConfigured = errors.New("no step configured")
var ErrNoCoreStepDeclared = errors.New("no core step type declared (e.g. get, put, task, etc.)")

// UnmarshalJSON unmarshals step configuration in multiple passes, determining
// precedence by the order of StepDetectors listed in the StepPrecedence
// variable.
//
// First, the step data is unmarshalled into a map[string]*json.RawMessage. Next,
// UnmarshalJSON loops over StepPrecedence to determine the type of step.
//
// For any StepDetector with a .Key field present in the map, .New is called to
// construct an empty StepConfig, and then json.Unmarshal is called on it to parse
// the data.
//
// For step modifiers like `timeout:` and `attempts:` they eventuallly wrap a
// core step type (e.g. get, put, task etc.). Core step types do not wrap other
// steps.
//
// When a core step type is encountered parsing stops and any remaining keys in
// rawStepConfig are considered invalid. This is how we stop someone from
// putting a `get` and `put` in the same step while still allowing valid step
// modifiers. This is also why step modifiers are listed first in
// StepPrecedence.
//
// If no StepDetectors match, no step is parsed, ErrNoStepConfigured is
// returned.
func (step *Step) UnmarshalJSON(data []byte) error {
	var rawStepConfig map[string]*json.RawMessage
	err := json.Unmarshal(data, &rawStepConfig)
	if err != nil {
		return err
	}

	var prevStep StepWrapper
	var coreStepDeclared bool
	for _, s := range StepPrecedence {
		_, found := rawStepConfig[s.Key]
		if !found {
			continue
		}

		curStep := s.New()

		err := json.Unmarshal(data, curStep)
		if err != nil {
			return MalformedStepError{
				StepType: s.Key,
				Err:      err,
			}
		}

		if step.Config == nil {
			step.Config = curStep
		}

		if prevStep != nil {
			prevStep.Wrap(curStep)
		}

		deleteKnownFields(rawStepConfig, curStep)

		if wrapper, isWrapper := curStep.(StepWrapper); isWrapper {
			prevStep = wrapper
		} else {
			coreStepDeclared = true
			break
		}

		data, err = json.Marshal(rawStepConfig)
		if err != nil {
			return fmt.Errorf("re-marshal rawStepConfig parsing: %w", err)
		}
	}

	if step.Config == nil {
		return ErrNoStepConfigured
	}

	if !coreStepDeclared {
		return ErrNoCoreStepDeclared
	}

	if len(rawStepConfig) != 0 {
		step.UnknownFields = rawStepConfig
	}

	return nil
}

// MarshalJSON marshals step configuration in multiple passes, looping and
// calling .Unwrap to marshal all nested steps into one big set of fields which
// is then marshalled and returned.
func (step Step) MarshalJSON() ([]byte, error) {
	fields := step.UnknownFields

	unwrapped := step.Config
	for unwrapped != nil {
		payload, err := json.Marshal(unwrapped)
		if err != nil {
			return nil, err
		}

		err = json.Unmarshal(payload, &fields)
		if err != nil {
			return nil, err
		}

		if wrapper, isWrapper := unwrapped.(StepWrapper); isWrapper {
			unwrapped = wrapper.Unwrap()
		} else {
			break
		}
	}

	return json.Marshal(fields)
}

// See the note about json tags here: https://golang.org/pkg/encoding/json/#Marshal
func deleteKnownFields(rawStepConfig map[string]*json.RawMessage, step StepConfig) {
	stepType := reflect.TypeOf(step).Elem()
	for i := 0; i < stepType.NumField(); i++ {
		field := stepType.Field(i)
		jsonTag := field.Tag.Get("json")
		if jsonTag == "-" {
			continue
		}
		jsonTagParts := strings.Split(jsonTag, ",")
		if len(jsonTagParts) < 1 {
			continue
		}
		jsonKey := jsonTagParts[0]
		if jsonKey == "" {
			jsonKey = field.Name
		}
		delete(rawStepConfig, jsonKey)
	}
}

// StepConfig is implemented by all step types.
type StepConfig interface {
	// Visit must call StepVisitor with the appropriate method corresponding to
	// this step type.
	//
	// When a new step type is added, the StepVisitor interface must be extended.
	// This allows the compiler to help us track down all the places where steps
	// must be handled type-by-type.
	Visit(StepVisitor) error
}

// StepWrapper is an optional interface for step types that is implemented by
// steps that wrap/modify other steps (e.g. hooks like `on_success`, `timeout`, etc.)
type StepWrapper interface {
	// Wrap is called during (Step).UnmarshalJSON whenever an 'inner' step is
	// parsed.
	//
	// Modifier step types should implement this function by assigning to an
	// internal field, that has a `json:"-"` tag, the passed in StepConfig.
	Wrap(StepConfig)

	// Unwrap is called during (Step).MarshalJSON and must return the wrapped
	// StepConfig.
	Unwrap() StepConfig
}

// StepVisitor is an interface used to assist in finding all the places that
// need to be updated whenever a new step type is introduced.
//
// Each StepConfig must implement .Visit to call the appropriate method on the
// given StepVisitor.
type StepVisitor interface {
	VisitTask(*TaskStep) error
	VisitGet(*GetStep) error
	VisitPut(*PutStep) error
	VisitSetPipeline(*SetPipelineStep) error
	VisitLoadVar(*LoadVarStep) error
	VisitTry(*TryStep) error
	VisitDo(*DoStep) error
	VisitInParallel(*InParallelStep) error
	VisitAggregate(*AggregateStep) error
	VisitTimeout(*TimeoutStep) error
	VisitRetry(*RetryStep) error
	VisitOnSuccess(*OnSuccessStep) error
	VisitOnFailure(*OnFailureStep) error
	VisitOnAbort(*OnAbortStep) error
	VisitOnError(*OnErrorStep) error
{'fraction_non_alphanumeric': 0.0986910994764398, 'fraction_numerical': 0.0006980802792321117, 'mean_word_length': 3.7029134181370535, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '705314', 'n_tokens_mistral': 3715, 'n_tokens_neox': 3435, 'n_words': 1268}