From 87947527d62bea7c5451a04c83274945a9e92728 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 28 Mar 2026 15:43:13 -0500 Subject: [PATCH] conver midi.message.create into separate processors --- .../processor/midi-control_change-create.go | 134 ++++++ internal/processor/midi-message-create.go | 439 ------------------ internal/processor/midi-note_off-create.go | 132 ++++++ internal/processor/midi-note_on-create.go | 132 ++++++ .../processor/midi-program_change-create.go | 106 +++++ .../test/midi-control_change-create_test.go | 161 +++++++ .../test/midi-message-create_test.go | 294 ------------ .../test/midi-note_off-create_test.go | 160 +++++++ .../test/midi-note_on-create_test.go | 157 +++++++ .../test/midi-program_change-create_test.go | 147 ++++++ 10 files changed, 1129 insertions(+), 733 deletions(-) create mode 100644 internal/processor/midi-control_change-create.go delete mode 100644 internal/processor/midi-message-create.go create mode 100644 internal/processor/midi-note_off-create.go create mode 100644 internal/processor/midi-note_on-create.go create mode 100644 internal/processor/midi-program_change-create.go create mode 100644 internal/processor/test/midi-control_change-create_test.go delete mode 100644 internal/processor/test/midi-message-create_test.go create mode 100644 internal/processor/test/midi-note_off-create_test.go create mode 100644 internal/processor/test/midi-note_on-create_test.go create mode 100644 internal/processor/test/midi-program_change-create_test.go diff --git a/internal/processor/midi-control_change-create.go b/internal/processor/midi-control_change-create.go new file mode 100644 index 0000000..90b4c28 --- /dev/null +++ b/internal/processor/midi-control_change-create.go @@ -0,0 +1,134 @@ +//go:build cgo + +package processor + +import ( + "bytes" + "context" + "fmt" + "strconv" + "text/template" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "gitlab.com/gomidi/midi/v2" +) + +type MIDIControlChangeCreate struct { + config config.ProcessorConfig + Channel *template.Template + Control *template.Template + Value *template.Template +} + +func (mccc *MIDIControlChangeCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { + templateData := wrappedPayload + + var channelBuffer bytes.Buffer + err := mccc.Channel.Execute(&channelBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) + + var controlBuffer bytes.Buffer + err = mccc.Control.Execute(&controlBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + controlValue, err := strconv.ParseUint(controlBuffer.String(), 10, 8) + + var valueBuffer bytes.Buffer + err = mccc.Value.Execute(&valueBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8) + + payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controlValue), uint8(valueValue)) + wrappedPayload.Payload = payloadMessage + return wrappedPayload, nil +} + +func (mccc *MIDIControlChangeCreate) Type() string { + return mccc.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "midi.control_change.create", + Title: "Create MIDI Control Change Message", + ParamsSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "channel": { + Title: "Channel", + Type: "string", + }, + "control": { + Title: "Control", + Type: "string", + }, + "value": { + Title: "Value", + Type: "string", + }, + }, + Required: []string{"channel", "control", "value"}, + AdditionalProperties: nil, + }, + New: func(config config.ProcessorConfig) (Processor, error) { + + params := config.Params + + channelString, err := params.GetString("channel") + if err != nil { + return nil, fmt.Errorf("midi.control_change.create channel error: %w", err) + } + + channelTemplate, err := template.New("channel").Parse(channelString) + + if err != nil { + return nil, err + } + + controlString, err := params.GetString("control") + if err != nil { + return nil, fmt.Errorf("midi.control_change.create control error: %w", err) + } + + controlTemplate, err := template.New("control").Parse(controlString) + + if err != nil { + return nil, err + } + + valueString, err := params.GetString("value") + if err != nil { + return nil, fmt.Errorf("midi.control_change.create value error: %w", err) + } + + valueTemplate, err := template.New("value").Parse(valueString) + + if err != nil { + return nil, err + } + return &MIDIControlChangeCreate{ + config: config, + Channel: channelTemplate, + Control: controlTemplate, + Value: valueTemplate, + }, nil + }, + }) +} diff --git a/internal/processor/midi-message-create.go b/internal/processor/midi-message-create.go deleted file mode 100644 index a4291be..0000000 --- a/internal/processor/midi-message-create.go +++ /dev/null @@ -1,439 +0,0 @@ -//go:build cgo - -package processor - -import ( - "bytes" - "context" - "fmt" - "strconv" - "text/template" - - "github.com/google/jsonschema-go/jsonschema" - "github.com/jwetzell/showbridge-go/internal/common" - "github.com/jwetzell/showbridge-go/internal/config" - "gitlab.com/gomidi/midi/v2" -) - -// TODO(jwetzell): support using numbers in config file treated as hardcoded values -type MIDIMessageCreate struct { - config config.ProcessorConfig - ProcessFunc func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) -} - -func (mmc *MIDIMessageCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { - return mmc.ProcessFunc(ctx, wrappedPayload) -} - -func (mmc *MIDIMessageCreate) Type() string { - return mmc.config.Type -} - -func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) { - - params := config.Params - - channelString, err := params.GetString("channel") - if err != nil { - return nil, fmt.Errorf("midi.message.create channel error: %w", err) - } - - channelTemplate, err := template.New("channel").Parse(channelString) - - if err != nil { - return nil, err - } - - noteString, err := params.GetString("note") - if err != nil { - return nil, fmt.Errorf("midi.message.create note error: %w", err) - } - - noteTemplate, err := template.New("note").Parse(noteString) - - if err != nil { - return nil, err - } - - velocityString, err := params.GetString("velocity") - if err != nil { - return nil, fmt.Errorf("midi.message.create velocity error: %w", err) - } - - velocityTemplate, err := template.New("velocity").Parse(velocityString) - - if err != nil { - return nil, err - } - - return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { - templateData := wrappedPayload - - var channelBuffer bytes.Buffer - err := channelTemplate.Execute(&channelBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) - - var noteBuffer bytes.Buffer - err = noteTemplate.Execute(¬eBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8) - - var velocityBuffer bytes.Buffer - err = velocityTemplate.Execute(&velocityBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8) - payloadMessage := midi.NoteOn(uint8(channelValue), uint8(noteValue), uint8(velocityValue)) - wrappedPayload.Payload = payloadMessage - return wrappedPayload, nil - }}, nil -} - -func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) { - - params := config.Params - - channelString, err := params.GetString("channel") - if err != nil { - return nil, fmt.Errorf("midi.message.create channel error: %w", err) - } - - channelTemplate, err := template.New("channel").Parse(channelString) - - if err != nil { - return nil, err - } - - noteString, err := params.GetString("note") - if err != nil { - return nil, fmt.Errorf("midi.message.create note error: %w", err) - } - - noteTemplate, err := template.New("note").Parse(noteString) - - if err != nil { - return nil, err - } - - velocityString, err := params.GetString("velocity") - if err != nil { - return nil, fmt.Errorf("midi.message.create velocity error: %w", err) - } - - velocityTemplate, err := template.New("velocity").Parse(velocityString) - - if err != nil { - return nil, err - } - - return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { - - templateData := wrappedPayload - - var channelBuffer bytes.Buffer - err := channelTemplate.Execute(&channelBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) - - var noteBuffer bytes.Buffer - err = noteTemplate.Execute(¬eBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8) - - var velocityBuffer bytes.Buffer - err = velocityTemplate.Execute(&velocityBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8) - - payloadMessage := midi.NoteOffVelocity(uint8(channelValue), uint8(noteValue), uint8(velocityValue)) - wrappedPayload.Payload = payloadMessage - return wrappedPayload, nil - }}, nil -} - -func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error) { - - params := config.Params - - channelString, err := params.GetString("channel") - if err != nil { - return nil, fmt.Errorf("midi.message.create channel error: %w", err) - } - - channelTemplate, err := template.New("channel").Parse(channelString) - - if err != nil { - return nil, err - } - - controlString, err := params.GetString("control") - if err != nil { - return nil, fmt.Errorf("midi.message.create control error: %w", err) - } - - controlTemplate, err := template.New("control").Parse(controlString) - - if err != nil { - return nil, err - } - - valueString, err := params.GetString("value") - if err != nil { - return nil, fmt.Errorf("midi.message.create value error: %w", err) - } - - valueTemplate, err := template.New("value").Parse(valueString) - - if err != nil { - return nil, err - } - - return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { - - templateData := wrappedPayload - - var channelBuffer bytes.Buffer - err := channelTemplate.Execute(&channelBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) - - var controlBuffer bytes.Buffer - err = controlTemplate.Execute(&controlBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - controlValue, err := strconv.ParseUint(controlBuffer.String(), 10, 8) - - var valueBuffer bytes.Buffer - err = valueTemplate.Execute(&valueBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8) - - payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controlValue), uint8(valueValue)) - wrappedPayload.Payload = payloadMessage - return wrappedPayload, nil - }}, nil -} - -func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error) { - - params := config.Params - - channelString, err := params.GetString("channel") - if err != nil { - return nil, fmt.Errorf("midi.message.create channel error: %w", err) - } - - channelTemplate, err := template.New("channel").Parse(channelString) - - if err != nil { - return nil, err - } - - programString, err := params.GetString("program") - if err != nil { - return nil, fmt.Errorf("midi.message.create program error: %w", err) - } - - programTemplate, err := template.New("program").Parse(programString) - - if err != nil { - return nil, err - } - - return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { - templateData := wrappedPayload - - var channelBuffer bytes.Buffer - err := channelTemplate.Execute(&channelBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) - - var programBuffer bytes.Buffer - err = programTemplate.Execute(&programBuffer, templateData) - - if err != nil { - wrappedPayload.End = true - return wrappedPayload, err - } - - programValue, err := strconv.ParseUint(programBuffer.String(), 10, 8) - - payloadMessage := midi.ProgramChange(uint8(channelValue), uint8(programValue)) - wrappedPayload.Payload = payloadMessage - return wrappedPayload, nil - }}, nil -} - -func init() { - RegisterProcessor(ProcessorRegistration{ - Type: "midi.message.create", - Title: "Create MIDI Message", - ParamsSchema: &jsonschema.Schema{ - Type: "object", - OneOf: []*jsonschema.Schema{ - { - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "type": { - Title: "MIDI Message Type", - Type: "string", - Enum: []any{"NoteOn", "noteon", "note_on"}, - }, - "channel": { - Title: "Channel", - Type: "string", - }, - "note": { - Title: "Note", - Type: "string", - }, - "velocity": { - Title: "Velocity", - Type: "string", - }, - }, - Required: []string{"type", "channel", "note", "velocity"}, - AdditionalProperties: nil, - }, - { - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "type": { - Title: "MIDI Message Type", - Type: "string", - Enum: []any{"NoteOff", "noteoff", "note_off"}, - }, - "channel": { - Title: "Channel", - Type: "string", - }, - "note": { - Title: "Note", - Type: "string", - }, - "velocity": { - Title: "Velocity", - Type: "string", - }, - }, - Required: []string{"type", "channel", "note", "velocity"}, - AdditionalProperties: nil, - }, - { - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "type": { - Title: "MIDI Message Type", - Type: "string", - Enum: []any{"ControlChange", "controlchange", "control_change"}, - }, - "channel": { - Title: "Channel", - Type: "string", - }, - "control": { - Title: "Control", - Type: "string", - }, - "value": { - Title: "Value", - Type: "string", - }, - }, - Required: []string{"type", "channel", "control", "value"}, - AdditionalProperties: nil, - }, - { - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "type": { - Title: "MIDI Message Type", - Type: "string", - Enum: []any{"ProgramChange", "programchange", "program_change"}, - }, - "channel": { - Title: "Channel", - Type: "string", - }, - "program": { - Title: "Program", - Type: "string", - }, - }, - Required: []string{"type", "channel", "program"}, - AdditionalProperties: nil, - }, - }, - }, - New: func(config config.ProcessorConfig) (Processor, error) { - params := config.Params - - msgTypeString, err := params.GetString("type") - if err != nil { - return nil, fmt.Errorf("midi.message.create type error: %w", err) - } - - switch msgTypeString { - case "NoteOn", "noteon", "note_on": - return newMidiNoteOnCreate(config) - case "NoteOff", "noteoff", "note_off": - return newMidiNoteOffCreate(config) - case "ControlChange", "controlchange", "control_change": - return newMidiControlChangeCreate(config) - case "ProgramChange", "programchange", "program_change": - return newMidiProgramChangeCreate(config) - default: - return nil, fmt.Errorf("midi.message.create does not support type %s", msgTypeString) - } - }, - }) -} diff --git a/internal/processor/midi-note_off-create.go b/internal/processor/midi-note_off-create.go new file mode 100644 index 0000000..c579e97 --- /dev/null +++ b/internal/processor/midi-note_off-create.go @@ -0,0 +1,132 @@ +//go:build cgo + +package processor + +import ( + "bytes" + "context" + "fmt" + "strconv" + "text/template" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "gitlab.com/gomidi/midi/v2" +) + +type MIDINoteOffCreate struct { + config config.ProcessorConfig + Channel *template.Template + Note *template.Template + Velocity *template.Template +} + +func (mnoc *MIDINoteOffCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { + templateData := wrappedPayload + + var channelBuffer bytes.Buffer + err := mnoc.Channel.Execute(&channelBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) + + var noteBuffer bytes.Buffer + err = mnoc.Note.Execute(¬eBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8) + + var velocityBuffer bytes.Buffer + err = mnoc.Velocity.Execute(&velocityBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8) + payloadMessage := midi.NoteOffVelocity(uint8(channelValue), uint8(noteValue), uint8(velocityValue)) + wrappedPayload.Payload = payloadMessage + return wrappedPayload, nil +} + +func (mnoc *MIDINoteOffCreate) Type() string { + return mnoc.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "midi.note_off.create", + Title: "Create MIDI Note Off Message", + ParamsSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "channel": { + Title: "Channel", + Type: "string", + }, + "note": { + Title: "Note", + Type: "string", + }, + "velocity": { + Title: "Velocity", + Type: "string", + }, + }, + Required: []string{"channel", "note", "velocity"}, + AdditionalProperties: nil, + }, + New: func(config config.ProcessorConfig) (Processor, error) { + params := config.Params + + channelString, err := params.GetString("channel") + if err != nil { + return nil, fmt.Errorf("midi.note_off.create channel error: %w", err) + } + + channelTemplate, err := template.New("channel").Parse(channelString) + + if err != nil { + return nil, err + } + + noteString, err := params.GetString("note") + if err != nil { + return nil, fmt.Errorf("midi.note_off.create note error: %w", err) + } + + noteTemplate, err := template.New("note").Parse(noteString) + + if err != nil { + return nil, err + } + + velocityString, err := params.GetString("velocity") + if err != nil { + return nil, fmt.Errorf("midi.note_off.create velocity error: %w", err) + } + + velocityTemplate, err := template.New("velocity").Parse(velocityString) + + if err != nil { + return nil, err + } + return &MIDINoteOffCreate{ + config: config, + Channel: channelTemplate, + Note: noteTemplate, + Velocity: velocityTemplate, + }, nil + }, + }) +} diff --git a/internal/processor/midi-note_on-create.go b/internal/processor/midi-note_on-create.go new file mode 100644 index 0000000..6b7f810 --- /dev/null +++ b/internal/processor/midi-note_on-create.go @@ -0,0 +1,132 @@ +//go:build cgo + +package processor + +import ( + "bytes" + "context" + "fmt" + "strconv" + "text/template" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "gitlab.com/gomidi/midi/v2" +) + +type MIDINoteOnCreate struct { + config config.ProcessorConfig + Channel *template.Template + Note *template.Template + Velocity *template.Template +} + +func (mnoc *MIDINoteOnCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { + templateData := wrappedPayload + + var channelBuffer bytes.Buffer + err := mnoc.Channel.Execute(&channelBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) + + var noteBuffer bytes.Buffer + err = mnoc.Note.Execute(¬eBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8) + + var velocityBuffer bytes.Buffer + err = mnoc.Velocity.Execute(&velocityBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8) + payloadMessage := midi.NoteOn(uint8(channelValue), uint8(noteValue), uint8(velocityValue)) + wrappedPayload.Payload = payloadMessage + return wrappedPayload, nil +} + +func (mnoc *MIDINoteOnCreate) Type() string { + return mnoc.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "midi.note_on.create", + Title: "Create MIDI Note On Message", + ParamsSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "channel": { + Title: "Channel", + Type: "string", + }, + "note": { + Title: "Note", + Type: "string", + }, + "velocity": { + Title: "Velocity", + Type: "string", + }, + }, + Required: []string{"channel", "note", "velocity"}, + AdditionalProperties: nil, + }, + New: func(config config.ProcessorConfig) (Processor, error) { + params := config.Params + + channelString, err := params.GetString("channel") + if err != nil { + return nil, fmt.Errorf("midi.note_on.create channel error: %w", err) + } + + channelTemplate, err := template.New("channel").Parse(channelString) + + if err != nil { + return nil, err + } + + noteString, err := params.GetString("note") + if err != nil { + return nil, fmt.Errorf("midi.note_on.create note error: %w", err) + } + + noteTemplate, err := template.New("note").Parse(noteString) + + if err != nil { + return nil, err + } + + velocityString, err := params.GetString("velocity") + if err != nil { + return nil, fmt.Errorf("midi.note_on.create velocity error: %w", err) + } + + velocityTemplate, err := template.New("velocity").Parse(velocityString) + + if err != nil { + return nil, err + } + return &MIDINoteOnCreate{ + config: config, + Channel: channelTemplate, + Note: noteTemplate, + Velocity: velocityTemplate, + }, nil + }, + }) +} diff --git a/internal/processor/midi-program_change-create.go b/internal/processor/midi-program_change-create.go new file mode 100644 index 0000000..6979821 --- /dev/null +++ b/internal/processor/midi-program_change-create.go @@ -0,0 +1,106 @@ +//go:build cgo + +package processor + +import ( + "bytes" + "context" + "fmt" + "strconv" + "text/template" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "gitlab.com/gomidi/midi/v2" +) + +type MIDIProgramChangeCreate struct { + config config.ProcessorConfig + Channel *template.Template + Program *template.Template +} + +func (mpcc *MIDIProgramChangeCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { + templateData := wrappedPayload + + var channelBuffer bytes.Buffer + err := mpcc.Channel.Execute(&channelBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) + + var programBuffer bytes.Buffer + err = mpcc.Program.Execute(&programBuffer, templateData) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + programValue, err := strconv.ParseUint(programBuffer.String(), 10, 8) + + payloadMessage := midi.ProgramChange(uint8(channelValue), uint8(programValue)) + wrappedPayload.Payload = payloadMessage + return wrappedPayload, nil +} + +func (mpcc *MIDIProgramChangeCreate) Type() string { + return mpcc.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "midi.program_change.create", + Title: "Create MIDI Prgoram Change Message", + ParamsSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "channel": { + Title: "Channel", + Type: "string", + }, + "program": { + Title: "Program", + Type: "string", + }, + }, + Required: []string{"type", "channel", "program"}, + AdditionalProperties: nil, + }, + New: func(config config.ProcessorConfig) (Processor, error) { + params := config.Params + + channelString, err := params.GetString("channel") + if err != nil { + return nil, fmt.Errorf("midi.program_change.create channel error: %w", err) + } + + channelTemplate, err := template.New("channel").Parse(channelString) + + if err != nil { + return nil, err + } + + programString, err := params.GetString("program") + if err != nil { + return nil, fmt.Errorf("midi.program_change.create program error: %w", err) + } + + programTemplate, err := template.New("program").Parse(programString) + + if err != nil { + return nil, err + } + return &MIDIProgramChangeCreate{ + config: config, + Channel: channelTemplate, + Program: programTemplate, + }, nil + }, + }) +} diff --git a/internal/processor/test/midi-control_change-create_test.go b/internal/processor/test/midi-control_change-create_test.go new file mode 100644 index 0000000..060acd8 --- /dev/null +++ b/internal/processor/test/midi-control_change-create_test.go @@ -0,0 +1,161 @@ +package processor_test + +import ( + "reflect" + "testing" + + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" + "gitlab.com/gomidi/midi/v2" +) + +func TestMIDIControlChangeCreateFromRegistry(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.control_change.create"] + if !ok { + t.Fatalf("midi.control_change.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.control_change.create", + Params: map[string]any{ + "channel": "1", + "control": "60", + "value": "100", + }, + }) + + if err != nil { + t.Fatalf("failed to create midi.control_change.create processor: %s", err) + } + + if processorInstance.Type() != "midi.control_change.create" { + t.Fatalf("midi.control_change.create processor has wrong type: %s", processorInstance.Type()) + } +} + +func TestGoodMIDIControlChangeCreate(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + expected any + }{ + { + name: "control_change message", + params: map[string]any{ + "type": "control_change", + "channel": "1", + "control": "64", + "value": "127", + }, + payload: "test", + expected: midi.ControlChange(1, 64, 127), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.control_change.create"] + if !ok { + t.Fatalf("midi.control_change.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.control_change.create", + Params: test.params, + }) + + if err != nil { + t.Fatalf("midi.control_change.create failed to create processor: %s", err) + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + if err != nil { + t.Fatalf("midi.control_change.create processing failed: %s", err) + } + + gotMessage, ok := got.Payload.(midi.Message) + if !ok { + t.Fatalf("midi.control_change.create returned a %T payload: %+v", got, got) + } + + if !reflect.DeepEqual(gotMessage, test.expected) { + t.Fatalf("midi.control_change.create got %v, expected %v", gotMessage, test.expected) + } + }) + } +} + +func TestBadMIDIControlChangeCreate(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + errorString string + }{ + { + name: "control_change no channel", + params: map[string]any{ + "type": "control_change", + "control": "64", + "value": "127", + }, + payload: "test", + errorString: "midi.control_change.create channel error: not found", + }, + { + name: "control_change no control", + params: map[string]any{ + "type": "control_change", + "channel": "1", + "value": "127", + }, + payload: "test", + errorString: "midi.control_change.create control error: not found", + }, + { + name: "control_change no value", + params: map[string]any{ + "type": "control_change", + "channel": "1", + "control": "64", + }, + payload: "test", + errorString: "midi.control_change.create value error: not found", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.control_change.create"] + if !ok { + t.Fatalf("midi.control_change.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.control_change.create", + Params: test.params, + }) + + if err != nil { + if test.errorString != err.Error() { + t.Fatalf("midi.control_change.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + return + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + + if err == nil { + t.Fatalf("midi.control_change.create expected to fail but succeeded, got: %v", got) + } + + if err.Error() != test.errorString { + t.Fatalf("midi.control_change.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + }) + } +} diff --git a/internal/processor/test/midi-message-create_test.go b/internal/processor/test/midi-message-create_test.go deleted file mode 100644 index b121c5d..0000000 --- a/internal/processor/test/midi-message-create_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package processor_test - -import ( - "reflect" - "testing" - - "github.com/jwetzell/showbridge-go/internal/common" - "github.com/jwetzell/showbridge-go/internal/config" - "github.com/jwetzell/showbridge-go/internal/processor" - "gitlab.com/gomidi/midi/v2" -) - -func TestMIDIMessageCreateFromRegistry(t *testing.T) { - registration, ok := processor.ProcessorRegistry["midi.message.create"] - if !ok { - t.Fatalf("midi.message.create processor not registered") - } - - processorInstance, err := registration.New(config.ProcessorConfig{ - Type: "midi.message.create", - Params: map[string]any{ - "type": "note_on", - "channel": "1", - "note": "60", - "velocity": "100", - }, - }) - - if err != nil { - t.Fatalf("failed to create midi.message.create processor: %s", err) - } - - if processorInstance.Type() != "midi.message.create" { - t.Fatalf("midi.message.create processor has wrong type: %s", processorInstance.Type()) - } -} - -func TestGoodMIDIMessageCreate(t *testing.T) { - - tests := []struct { - name string - params map[string]any - payload any - expected any - }{ - { - name: "note_on message", - params: map[string]any{ - "type": "note_on", - "channel": "1", - "note": "60", - "velocity": "100", - }, - payload: "test", - expected: midi.NoteOn(1, 60, 100), - }, - { - name: "note_off message", - params: map[string]any{ - "type": "note_off", - "channel": "1", - "note": "60", - "velocity": "100", - }, - payload: "test", - expected: midi.NoteOffVelocity(1, 60, 100), - }, - { - name: "control_change message", - params: map[string]any{ - "type": "control_change", - "channel": "1", - "control": "64", - "value": "127", - }, - payload: "test", - expected: midi.ControlChange(1, 64, 127), - }, - { - name: "program_change message", - params: map[string]any{ - "type": "program_change", - "channel": "1", - "program": "10", - }, - payload: "test", - expected: midi.ProgramChange(1, 10), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - registration, ok := processor.ProcessorRegistry["midi.message.create"] - if !ok { - t.Fatalf("midi.message.create processor not registered") - } - - processorInstance, err := registration.New(config.ProcessorConfig{ - Type: "midi.message.create", - Params: test.params, - }) - - if err != nil { - t.Fatalf("midi.message.create failed to create processor: %s", err) - } - - got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) - if err != nil { - t.Fatalf("midi.message.create processing failed: %s", err) - } - - gotMessage, ok := got.Payload.(midi.Message) - if !ok { - t.Fatalf("midi.message.create returned a %T payload: %+v", got, got) - } - - if !reflect.DeepEqual(gotMessage, test.expected) { - t.Fatalf("midi.message.create got %v, expected %v", gotMessage, test.expected) - } - }) - } -} - -func TestBadMIDIMessageCreate(t *testing.T) { - - tests := []struct { - name string - params map[string]any - payload any - errorString string - }{ - { - name: "no type parameter", - params: map[string]any{}, - payload: "test", - errorString: "midi.message.create type error: not found", - }, - { - name: "non-string type parameter", - params: map[string]any{ - "type": 1, - }, - payload: "test", - errorString: "midi.message.create type error: not a string", - }, - { - name: "unknown type parameter", - params: map[string]any{ - "type": "asdf", - }, - payload: "test", - errorString: "midi.message.create does not support type asdf", - }, - { - name: "note_on message no channel", - params: map[string]any{ - "type": "note_on", - "note": "60", - "velocity": "100", - }, - payload: "test", - errorString: "midi.message.create channel error: not found", - }, - { - name: "note_on message no note", - params: map[string]any{ - "type": "note_on", - "channel": "1", - "velocity": "100", - }, - payload: "test", - errorString: "midi.message.create note error: not found", - }, - { - name: "note_on message no velocity", - params: map[string]any{ - "type": "note_on", - "channel": "1", - "note": "60", - }, - payload: "test", - errorString: "midi.message.create velocity error: not found", - }, - { - name: "note_off message no channel", - params: map[string]any{ - "type": "note_off", - "note": "60", - "velocity": "100", - }, - payload: "test", - errorString: "midi.message.create channel error: not found", - }, - { - name: "note_off message no note", - params: map[string]any{ - "type": "note_off", - "channel": "1", - "velocity": "100", - }, - payload: "test", - errorString: "midi.message.create note error: not found", - }, - { - name: "note_off message no velocity", - params: map[string]any{ - "type": "note_off", - "channel": "1", - "note": "60", - }, - payload: "test", - errorString: "midi.message.create velocity error: not found", - }, - { - name: "control_change no channel", - params: map[string]any{ - "type": "control_change", - "control": "64", - "value": "127", - }, - payload: "test", - errorString: "midi.message.create channel error: not found", - }, - { - name: "control_change no control", - params: map[string]any{ - "type": "control_change", - "channel": "1", - "value": "127", - }, - payload: "test", - errorString: "midi.message.create control error: not found", - }, - { - name: "control_change no value", - params: map[string]any{ - "type": "control_change", - "channel": "1", - "control": "64", - }, - payload: "test", - errorString: "midi.message.create value error: not found", - }, - { - name: "program_change no channel", - params: map[string]any{ - "type": "program_change", - "program": "64", - }, - payload: "test", - errorString: "midi.message.create channel error: not found", - }, - { - name: "program_change no program", - params: map[string]any{ - "type": "program_change", - "channel": "1", - }, - payload: "test", - errorString: "midi.message.create program error: not found", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - registration, ok := processor.ProcessorRegistry["midi.message.create"] - if !ok { - t.Fatalf("midi.message.create processor not registered") - } - - processorInstance, err := registration.New(config.ProcessorConfig{ - Type: "midi.message.create", - Params: test.params, - }) - - if err != nil { - if test.errorString != err.Error() { - t.Fatalf("midi.message.create got error '%s', expected '%s'", err.Error(), test.errorString) - } - return - } - - got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) - - if err == nil { - t.Fatalf("midi.message.create expected to fail but succeeded, got: %v", got) - } - - if err.Error() != test.errorString { - t.Fatalf("midi.message.create got error '%s', expected '%s'", err.Error(), test.errorString) - } - }) - } -} diff --git a/internal/processor/test/midi-note_off-create_test.go b/internal/processor/test/midi-note_off-create_test.go new file mode 100644 index 0000000..ac6c48b --- /dev/null +++ b/internal/processor/test/midi-note_off-create_test.go @@ -0,0 +1,160 @@ +package processor_test + +import ( + "reflect" + "testing" + + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" + "gitlab.com/gomidi/midi/v2" +) + +func TestMIDINoteOffCreteaFromRegistry(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.note_off.create"] + if !ok { + t.Fatalf("midi.note_off.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.note_off.create", + Params: map[string]any{ + "channel": "1", + "note": "60", + "velocity": "100", + }, + }) + + if err != nil { + t.Fatalf("failed to create midi.note_off.create processor: %s", err) + } + + if processorInstance.Type() != "midi.note_off.create" { + t.Fatalf("midi.note_off.create processor has wrong type: %s", processorInstance.Type()) + } +} + +func TestGoodMIDINoteOffCretea(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + expected any + }{ + { + name: "note_off message", + params: map[string]any{ + "channel": "1", + "note": "60", + "velocity": "100", + }, + payload: "test", + expected: midi.NoteOffVelocity(1, 60, 100), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.note_off.create"] + if !ok { + t.Fatalf("midi.note_off.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.note_off.create", + Params: test.params, + }) + + if err != nil { + t.Fatalf("midi.note_off.create failed to create processor: %s", err) + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + if err != nil { + t.Fatalf("midi.note_off.create processing failed: %s", err) + } + + gotMessage, ok := got.Payload.(midi.Message) + if !ok { + t.Fatalf("midi.note_off.create returned a %T payload: %+v", got, got) + } + + if !reflect.DeepEqual(gotMessage, test.expected) { + t.Fatalf("midi.note_off.create got %v, expected %v", gotMessage, test.expected) + } + }) + } +} + +func TestBadMIDINoteOffCretea(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + errorString string + }{ + { + name: "note_off message no channel", + params: map[string]any{ + "type": "note_off", + "note": "60", + "velocity": "100", + }, + payload: "test", + errorString: "midi.note_off.create channel error: not found", + }, + { + name: "note_off message no note", + params: map[string]any{ + "type": "note_off", + "channel": "1", + "velocity": "100", + }, + payload: "test", + errorString: "midi.note_off.create note error: not found", + }, + { + name: "note_off message no velocity", + params: map[string]any{ + "type": "note_off", + "channel": "1", + "note": "60", + }, + payload: "test", + errorString: "midi.note_off.create velocity error: not found", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.note_off.create"] + if !ok { + t.Fatalf("midi.note_off.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.note_off.create", + Params: test.params, + }) + + if err != nil { + if test.errorString != err.Error() { + t.Fatalf("midi.note_off.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + return + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + + if err == nil { + t.Fatalf("midi.note_off.create expected to fail but succeeded, got: %v", got) + } + + if err.Error() != test.errorString { + t.Fatalf("midi.note_off.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + }) + } +} diff --git a/internal/processor/test/midi-note_on-create_test.go b/internal/processor/test/midi-note_on-create_test.go new file mode 100644 index 0000000..9867c9d --- /dev/null +++ b/internal/processor/test/midi-note_on-create_test.go @@ -0,0 +1,157 @@ +package processor_test + +import ( + "reflect" + "testing" + + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" + "gitlab.com/gomidi/midi/v2" +) + +func TestMIDINoteOnCreateFromRegistry(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.note_on.create"] + if !ok { + t.Fatalf("midi.note_on.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.note_on.create", + Params: map[string]any{ + "channel": "1", + "note": "60", + "velocity": "100", + }, + }) + + if err != nil { + t.Fatalf("failed to create midi.note_on.create processor: %s", err) + } + + if processorInstance.Type() != "midi.note_on.create" { + t.Fatalf("midi.note_on.create processor has wrong type: %s", processorInstance.Type()) + } +} + +func TestGoodMIDINoteOnCreate(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + expected any + }{ + { + name: "note_on message", + params: map[string]any{ + "channel": "1", + "note": "60", + "velocity": "100", + }, + payload: "test", + expected: midi.NoteOn(1, 60, 100), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.note_on.create"] + if !ok { + t.Fatalf("midi.note_on.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.note_on.create", + Params: test.params, + }) + + if err != nil { + t.Fatalf("midi.note_on.create failed to create processor: %s", err) + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + if err != nil { + t.Fatalf("midi.note_on.create processing failed: %s", err) + } + + gotMessage, ok := got.Payload.(midi.Message) + if !ok { + t.Fatalf("midi.note_on.create returned a %T payload: %+v", got, got) + } + + if !reflect.DeepEqual(gotMessage, test.expected) { + t.Fatalf("midi.note_on.create got %v, expected %v", gotMessage, test.expected) + } + }) + } +} + +func TestBadMIDINoteOnCreate(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + errorString string + }{ + { + name: "note_on message no channel", + params: map[string]any{ + "note": "60", + "velocity": "100", + }, + payload: "test", + errorString: "midi.note_on.create channel error: not found", + }, + { + name: "note_on message no note", + params: map[string]any{ + "channel": "1", + "velocity": "100", + }, + payload: "test", + errorString: "midi.note_on.create note error: not found", + }, + { + name: "note_on message no velocity", + params: map[string]any{ + "channel": "1", + "note": "60", + }, + payload: "test", + errorString: "midi.note_on.create velocity error: not found", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.note_on.create"] + if !ok { + t.Fatalf("midi.note_on.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.note_on.create", + Params: test.params, + }) + + if err != nil { + if test.errorString != err.Error() { + t.Fatalf("midi.note_on.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + return + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + + if err == nil { + t.Fatalf("midi.note_on.create expected to fail but succeeded, got: %v", got) + } + + if err.Error() != test.errorString { + t.Fatalf("midi.note_on.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + }) + } +} diff --git a/internal/processor/test/midi-program_change-create_test.go b/internal/processor/test/midi-program_change-create_test.go new file mode 100644 index 0000000..4289730 --- /dev/null +++ b/internal/processor/test/midi-program_change-create_test.go @@ -0,0 +1,147 @@ +package processor_test + +import ( + "reflect" + "testing" + + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" + "gitlab.com/gomidi/midi/v2" +) + +func TestMIDIProgramChangeCreateFromRegistry(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.program_change.create"] + if !ok { + t.Fatalf("midi.program_change.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.program_change.create", + Params: map[string]any{ + "channel": "1", + "program": "60", + }, + }) + + if err != nil { + t.Fatalf("failed to create midi.program_change.create processor: %s", err) + } + + if processorInstance.Type() != "midi.program_change.create" { + t.Fatalf("midi.program_change.create processor has wrong type: %s", processorInstance.Type()) + } +} + +func TestGoodMIDIProgramChangeCreate(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + expected any + }{ + { + name: "program_change message", + params: map[string]any{ + "type": "program_change", + "channel": "1", + "program": "10", + }, + payload: "test", + expected: midi.ProgramChange(1, 10), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.program_change.create"] + if !ok { + t.Fatalf("midi.program_change.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.program_change.create", + Params: test.params, + }) + + if err != nil { + t.Fatalf("midi.program_change.create failed to create processor: %s", err) + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + if err != nil { + t.Fatalf("midi.program_change.create processing failed: %s", err) + } + + gotMessage, ok := got.Payload.(midi.Message) + if !ok { + t.Fatalf("midi.program_change.create returned a %T payload: %+v", got, got) + } + + if !reflect.DeepEqual(gotMessage, test.expected) { + t.Fatalf("midi.program_change.create got %v, expected %v", gotMessage, test.expected) + } + }) + } +} + +func TestBadMIDIProgramChangeCreate(t *testing.T) { + + tests := []struct { + name string + params map[string]any + payload any + errorString string + }{ + { + name: "program_change no channel", + params: map[string]any{ + "type": "program_change", + "program": "64", + }, + payload: "test", + errorString: "midi.program_change.create channel error: not found", + }, + { + name: "program_change no program", + params: map[string]any{ + "type": "program_change", + "channel": "1", + }, + payload: "test", + errorString: "midi.program_change.create program error: not found", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["midi.program_change.create"] + if !ok { + t.Fatalf("midi.program_change.create processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "midi.program_change.create", + Params: test.params, + }) + + if err != nil { + if test.errorString != err.Error() { + t.Fatalf("midi.program_change.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + return + } + + got, err := processorInstance.Process(t.Context(), common.GetWrappedPayload(t.Context(), test.payload)) + + if err == nil { + t.Fatalf("midi.program_change.create expected to fail but succeeded, got: %v", got) + } + + if err.Error() != test.errorString { + t.Fatalf("midi.program_change.create got error '%s', expected '%s'", err.Error(), test.errorString) + } + }) + } +}