initial commit

pull/1/head
Thomas Martin 2024-11-21 17:47:49 +01:00
commit 920634d11b
229 changed files with 10942 additions and 0 deletions

128
README.md 100644
View File

@ -0,0 +1,128 @@
# cmg-ws202425
Cloud-native Microservices mit Go - WS 2024/25
# Green Compute Load Shifting (GCLS)
![green compute load shifting](doc/overview.png)
## Functional Requirements
1. Consumers can create new computation jobs. A container image along with a set of environment variables defines the computation job.
2. Consumers can get the results of a compute job. The result consists of the state, the standard output produced by the job, and an estimate of the amount of CO2 equivalent that has been been emitted while executing the job.
3. Providers can offer computing capabilities and run assigned jobs
4. Jobs are scheduled to minimize runtime and carbon footprint
## Quality Requirements - Functional Suitability
### Functional Correctness
The error margin for estimating carbon emission shall be $\frac{+}{-} 10\%$
### Functional Appropriateness
In each week, the system shall reduce the carbon emissions of running compute jobs by at least $50\%$ compared to running the jobs in the client region
## Quality Requirements - Performance Efficiency
### Time behavior
$95\%$ of all backend requests that originate from a user must be handled in less than $50 ms$
### Capacity
The system must be able to handle up to 10000 jobs per day.
### Resource utilization
- The CPU and Memory consumption of all jobs running on a worker must not exceed $50\%$ of the available resources of that worker.
- **The overall cloud provider costs must not exceed the value of the obtained Google Cloud Education Credits**
## Quality Requirements - Compatibility
### Interoperability
The system must support Docker container images that are available on [the docker hub](https://hub.docker.com/)
## Quality Requirements - Reliability
### Availability
In each 24h timeframe, the request success rate must be at least $95\%$
### Fault tolerance
The system must not reject requests in case the carbon intensity provider is temporarily not available.
## Quality Requirements - Security
### Authenticity
All inter service communication must be authenticated and authorized (Zero Trust)
### Confidentiality
- Compute Consumers and Compute Providers must ony be able to access job information assigned to them
## Quality Requirements - Maintainability
### Reusability
Cross cutting concerns shall be implemented consistently in all services
### Testability
- The business code must be testable in isolation
- The unit test coverage of the business code must be at least $80\%$
## Quality Requirements - Flexibility
### Replaceability
- The cloud provider must be replaceable without changing the business code
- The carbon intensity provider must be replaceable without redeploying more than one service
## Boundary Conditions - Architecure
1. The predefined microservices architecture must be followed (see next slides)
2. All services/clients must be implemented using the Go Programming Language
3. Each microservice needs to adhere to the [ports & adapters](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) structure
4. All services need to be stateless
5. All synchronous communication shall be handled using REST APIs
6. APIs shall be idempotent, breaking changes shall be avoided
7. Each service shall be containerized
## Boundary Conditions - Development
1. A single monorepo shall be used for all code <https://gitty.informatik.hs-mannheim.de/steger/cmg-ws202425.git>
2. All Infrastructure shall be defined as code (IaC) using [terraform](https://www.terraform.io/)
3. All features must be implemented in short lived feature branches
4. The usage of 3rd party packages (other than from the Go standard library) must be approved
5. All PRs must be reviewed by at least one other team member before a feature branch can be merged to main
6. All tests must be passed before a feature branch can be merged to main
## Boundary Conditions - Security
1. JWTs shall be used for all requests originating from a client
2. basic auth shall be used for all other communication
3. secrets must not be stored in the repository
## Boundary Conditions - Deployment
1. The entrire system shall be deployed to the [Google cloud platform](https://console.cloud.google.com/). The gcp project id is `cmg-ws202425`.
2. **The overall provider costs must not exceed the value of the obtained Google Cloud Education Credits**
3. All services shall be deployed to a CaaS or PaaS offering in a single GCP cloud environment
4. Each service has its own CI/CD pipeline
5. Deployments are done independetly for each service with zero downtime (Rolling updates)
## Boundary Conditions - Operations
1. All logs shall be written to standard output
2. Business, Application, and Infrastructure level metrics shall be collected by [prometheus](https://prometheus.io/).
3. All requests shall be traced using [jaeger](https://www.jaegertracing.io/)
4. each service must provide health probes for readiness and liveness
5. each service must terminate upon receiving the SIGTERM signal within 2 seconds.

View File

@ -0,0 +1,13 @@
# Entity Service
The entity service is an example service that demonstrates the folder structure of a microservice following the ports & adapters architecture.
> **WARNING**
> The implementation is in an early stage. Many things are still missing. Use with care.
## Usage
```bash
curl -X PUT -d '{ "Id": "34", "IntProp" : 23, "StringProp": "test" }' localhost:8080/entity
curl localhost:8080/entity/34
```

View File

@ -0,0 +1,33 @@
package jwt_store
import (
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type JWTStore struct {
//Gespeichert als Bytearray um manuelles überschreieben zu ermöglichen
jwt []byte
}
func NewJWTStore() *JWTStore {
return &JWTStore{}
}
func (s *JWTStore) GetJWT() string {
return string(s.jwt)
}
func (s *JWTStore) SetJWT(jwt string) {
s.jwt = []byte(jwt)
}
func (s *JWTStore) DeleteJWT() {
for i := range s.jwt {
s.jwt[i] = 0
}
s.jwt = nil
}
var _ ports.JWTStore = (*JWTStore)(nil) // Check if the Store struct implements the Communication interface

View File

@ -0,0 +1,25 @@
package jwt_store
import (
"testing"
)
func TestJWTStore(t *testing.T) {
store := NewJWTStore()
if store.GetJWT() != "" {
t.Error("Expected empty jwt, but got", store.GetJWT())
}
store.SetJWT("mySecret")
if store.GetJWT() != "mySecret" {
t.Error("Expected jwt to be set, but got", store.GetJWT())
}
store.DeleteJWT()
if store.GetJWT() != "" {
t.Error("Expected jwt to be deleted, but got", store.GetJWT())
}
}

View File

@ -0,0 +1,76 @@
package rest_client_mock
import (
"fmt"
"github.com/google/uuid"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type MockRestClient struct {
jobs map[uuid.UUID]ports.GetJobDto
}
func NewMockRestClient() *MockRestClient {
return &MockRestClient{jobs: make(map[uuid.UUID]ports.GetJobDto)}
}
func (d *MockRestClient) GetJob(id string) (*ports.GetJobDto, error) {
val, ok := d.jobs[uuid.MustParse(id)]
if !ok {
return nil, fmt.Errorf("job %s not found", id)
}
return &val, nil
}
func (d *MockRestClient) GetJobs() ([]ports.GetJobDto, error) {
jobs := []ports.GetJobDto{}
for _, v := range d.jobs {
jobs = append(jobs, v)
}
return jobs, nil
}
func (d *MockRestClient) CreateJobDto(dto ports.CreateJobDto) error {
job := dto.Job
id := uuid.New()
d.jobs[id] = ports.GetJobDto{
Id: id.String(),
Name: job.Name,
ConsumerId: "",
ImageName: job.ImageName,
EnvironmentVariables: job.EnvironmentVariables,
Status: "",
StandardOutput: "",
CreatedAt: 0,
StartedAt: 0,
FinishedAt: 0,
ConsumerLongitude: job.ConsumerLongitude,
ConsumerLatitude: job.ConsumerLatitude,
Co2EquivalentEmissionConsumer: 0,
EstimatedCo2Equivalent: 0,
}
return nil
}
func (d *MockRestClient) Login(dto ports.LoginDto) (*ports.LoginResponseDto, error) {
return &ports.LoginResponseDto{
Token: "aaaaaa",
}, nil
}
type MockLocationStore struct {
geolocation *ports.Geolocation
}
func (d *MockLocationStore) GetLocation() (*ports.Geolocation, error) {
if d.geolocation == nil {
return nil, fmt.Errorf("no geolocation store")
}
return d.geolocation, nil
}
func (d *MockLocationStore) SetLocation(latitude float64, longitude float64) error {
d.geolocation = &ports.Geolocation{Latitude: latitude, Longitude: longitude}
return nil
}

View File

@ -0,0 +1,117 @@
package rest_client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
const serverPort = 8080
var requestURL = fmt.Sprintf("http://localhost:%d/", serverPort)
type RestClient struct {
}
func NewRestClient() *RestClient {
h := RestClient{}
return &h
}
func (r *RestClient) GetJob(id string) (*ports.GetJobDto, error) {
var data *ports.GetJobDto
data = &ports.GetJobDto{}
res, err := http.Get(requestURL + "job/" + id)
if err != nil {
fmt.Printf("Error making http request: %s\n", err)
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("Error reading Response Body:", err)
return nil, err
}
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Println("Error parsing JSON:", err)
return nil, err
}
return data, nil
}
func (r *RestClient) GetJobs() ([]ports.GetJobDto, error) {
var data []ports.GetJobDto
res, err := http.Get(requestURL + "job")
if err != nil {
fmt.Printf("Error making http request: %s\n", err)
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("Error reading Response Body:", err)
return nil, err
}
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Println("Error parsing JSON:", err)
return nil, err
}
return data, nil
}
func (r *RestClient) CreateJobDto(dto ports.CreateJobDto) error {
jsonBody, err := json.Marshal(dto)
res, err := http.Post(requestURL+"job", "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
fmt.Printf("Error making http request: %s\n", err)
return err
}
if res.StatusCode != http.StatusOK {
fmt.Printf("Error making http request: %s\n", res.Status)
return err
}
fmt.Println("Success")
return nil
}
func (r *RestClient) Login(dto ports.LoginDto) (*ports.LoginResponseDto, error) {
var data *ports.LoginResponseDto
data = &ports.LoginResponseDto{}
jsonBody, err := json.Marshal(dto)
res, err := http.Post(requestURL+"login", "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
fmt.Printf("Error making http request: %s\n", err)
return nil, err
}
if res.StatusCode != http.StatusOK {
fmt.Printf("Error making http request: %s\n", res.Status)
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("Error reading Response Body:", err)
return nil, err
}
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Println("Error parsing JSON:", err)
return nil, err
}
return data, nil
}
var _ ports.RestClient = (*RestClient)(nil) // Check if the Store struct implements the Communication interface

View File

@ -0,0 +1,109 @@
package core
import (
"fmt"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type CLI struct {
commandHandler ports.Api
}
func NewCli(commandHandler ports.Api) *CLI {
return &CLI{commandHandler: commandHandler}
}
func (c *CLI) Scan(input string) {
commandParser := CommandParser{}
command, err := commandParser.ParseCommand(input)
if err != nil {
fmt.Printf("%s\n", err)
return
}
switch cmd := command.(type) {
case *ports.CreateJobCommand:
c.onCreateJobCommand(*cmd)
case *ports.GetAllJobsCommand:
c.onGetAllJobsCommand(*cmd)
case *ports.GetJobByIdCommand:
c.onGetJobByIdCommand(*cmd)
case *ports.LoginCommand:
c.onLoginCommand(*cmd)
case *ports.GetConsumerLocationCommand:
c.onGetConsumerLocationCommand(*cmd)
case *ports.SetConsumerLocationCommand:
c.onSetConsumerLocationCommand(*cmd)
case *ports.HelpCommand:
c.onHelpCommand(*cmd)
default:
fmt.Println("Please specify a valid command")
}
}
func (c *CLI) onCreateJobCommand(cmd ports.CreateJobCommand) {
result, err := c.commandHandler.HandleCreateJob(cmd)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("Created Job: %+v\n", result)
}
}
func (c *CLI) onGetAllJobsCommand(cmd ports.GetAllJobsCommand) {
result, err := c.commandHandler.HandleGetAllJobs(cmd)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
for _, job := range result {
c.printJob(job)
}
}
}
func (c *CLI) onGetJobByIdCommand(cmd ports.GetJobByIdCommand) {
result, err := c.commandHandler.HandleGetJobById(cmd)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
c.printJob(*result)
}
}
func (c *CLI) onLoginCommand(cmd ports.LoginCommand) {
_, err := c.commandHandler.HandleLogin(cmd)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("Successfully loged in\n")
}
}
func (c *CLI) onGetConsumerLocationCommand(cmd ports.GetConsumerLocationCommand) {
result, err := c.commandHandler.HandleGetConsumerLocation(cmd)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("Latitude: %f\nLongitude: %f\n", result.Latitude, result.Longitude)
}
}
func (c *CLI) onSetConsumerLocationCommand(cmd ports.SetConsumerLocationCommand) {
err := c.commandHandler.HandleSetConsumerLocation(cmd)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Println("Successfully set location")
}
}
func (c *CLI) onHelpCommand(cmd ports.HelpCommand) {
result := c.commandHandler.HandleHelp(cmd)
fmt.Println(result)
}
func (c *CLI) printJob(dto ports.GetJobDto) {
fmt.Printf("ID: %s\nName: %s\nImageName: %s\nEnviromental Variables: %s\nStatus: %s\nStandard Output: %s\nCreated at: %d\nStarted at: %d\nFinished at: %d\nConsumer Latitude: %f\nConsumer Longitude: %f\nCo2 Equivalent Emission Consumer: %f\nEstimated Co2 Equvaelnt: %f\n", dto.Id, dto.Name, dto.ImageName, dto.EnvironmentVariables, dto.Status, dto.StandardOutput, dto.CreatedAt, dto.StartedAt, dto.FinishedAt, dto.ConsumerLatitude, dto.ConsumerLongitude, dto.Co2EquivalentEmissionConsumer, dto.EstimatedCo2Equivalent)
fmt.Println()
}

View File

@ -0,0 +1,142 @@
package core
import (
"errors"
"testing"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type MockCommandHandler struct {
handleCreateJobFunc func(command ports.CreateJobCommand) (*ports.BaseJob, error)
handleGetJobByIdFunc func(command ports.GetJobByIdCommand) (*ports.GetJobDto, error)
handleGetAllJobsFunc func(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error)
handleLoginFunc func(command ports.LoginCommand) (*ports.LoginResponseDto, error)
handleHelpFunc func(command ports.HelpCommand) string
handleSetConsumerLocationFunc func(command ports.SetConsumerLocationCommand) error
handleGetConsumerLocationFunc func(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error)
}
func (m *MockCommandHandler) HandleCreateJob(command ports.CreateJobCommand) (*ports.BaseJob, error) {
return m.handleCreateJobFunc(command)
}
func (m *MockCommandHandler) HandleGetJobById(command ports.GetJobByIdCommand) (*ports.GetJobDto, error) {
return m.handleGetJobByIdFunc(command)
}
func (m *MockCommandHandler) HandleGetAllJobs(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error) {
return m.handleGetAllJobsFunc(command)
}
func (m *MockCommandHandler) HandleLogin(command ports.LoginCommand) (*ports.LoginResponseDto, error) {
return m.handleLoginFunc(command)
}
func (m *MockCommandHandler) HandleHelp(command ports.HelpCommand) string {
return m.handleHelpFunc(command)
}
func (m *MockCommandHandler) HandleSetConsumerLocation(command ports.SetConsumerLocationCommand) error {
return m.handleSetConsumerLocationFunc(command)
}
func (m *MockCommandHandler) HandleGetConsumerLocation(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error) {
return m.handleGetConsumerLocationFunc(command)
}
func TestCLI(t *testing.T) {
tests := []struct {
name string
input string
expectedFunction string
}{
{
name: "Create Job Command",
input: `job create "myJob" "myImage" "env1:value1" "env2:value2"`,
expectedFunction: "HandleCreateJob",
},
{
name: "Get All Jobs Command",
input: `job get`,
expectedFunction: "HandleGetAllJobs",
},
{
name: "Get Job By ID Command",
input: `job get 123`,
expectedFunction: "HandleGetJobById",
},
{
name: "Login Command",
input: `login "username" "password"`,
expectedFunction: "HandleLogin",
},
{
name: "Get Consumer Location Command",
input: `location get`,
expectedFunction: "HandleGetConsumerLocation",
},
{
name: "Set Consumer Location Command",
input: `location set 37.7749 -122.4194`,
expectedFunction: "HandleSetConsumerLocation",
},
{
name: "Help Command",
input: `help`,
expectedFunction: "HandleHelp",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockCommandHandler := &MockCommandHandler{
handleCreateJobFunc: func(command ports.CreateJobCommand) (*ports.BaseJob, error) {
if tt.expectedFunction != "HandleCreateJob" {
t.Errorf("Unexpected function call: HandleCreateJob")
}
return nil, errors.New("not implemented")
},
handleGetJobByIdFunc: func(command ports.GetJobByIdCommand) (*ports.GetJobDto, error) {
if tt.expectedFunction != "HandleGetJobById" {
t.Errorf("Unexpected function call: HandleGetJobById")
}
return nil, errors.New("not implemented")
},
handleGetAllJobsFunc: func(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error) {
if tt.expectedFunction != "HandleGetAllJobs" {
t.Errorf("Unexpected function call: HandleGetAllJobs")
}
return nil, errors.New("not implemented")
},
handleLoginFunc: func(command ports.LoginCommand) (*ports.LoginResponseDto, error) {
if tt.expectedFunction != "HandleLogin" {
t.Errorf("Unexpected function call: HandleLogin")
}
return nil, errors.New("not implemented")
},
handleHelpFunc: func(command ports.HelpCommand) string {
if tt.expectedFunction != "HandleHelp" {
t.Errorf("Unexpected function call: HandleHelp")
}
return ""
},
handleSetConsumerLocationFunc: func(command ports.SetConsumerLocationCommand) error {
if tt.expectedFunction != "HandleSetConsumerLocation" {
t.Errorf("Unexpected function call: HandleSetConsumerLocation")
}
return errors.New("not implemented")
},
handleGetConsumerLocationFunc: func(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error) {
if tt.expectedFunction != "HandleGetConsumerLocation" {
t.Errorf("Unexpected function call: HandleGetConsumerLocation")
}
return nil, errors.New("not implemented")
},
}
cli := NewCli(mockCommandHandler)
cli.Scan(tt.input)
})
}
}

View File

@ -0,0 +1,106 @@
package core
import (
"fmt"
location_store "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/core/location-store"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type CommandHandler struct {
restClient ports.RestClient
locationStore location_store.LocationStore
}
var _ ports.Api = (*CommandHandler)(nil)
func NewCommandHandler(restCl ports.RestClient, locationSt location_store.LocationStore) *CommandHandler {
return &CommandHandler{restClient: restCl, locationStore: locationSt}
}
func (h *CommandHandler) HandleCreateJob(command ports.CreateJobCommand) (*ports.BaseJob, error) {
location, err := h.locationStore.GetLocation()
if err != nil {
return nil, fmt.Errorf("unable to read location: %w", err)
}
createJobDto := ports.CreateJobDto{
Job: ports.BaseJob{
Name: command.JobName,
ImageName: command.ImageName,
EnvironmentVariables: command.EnvironmentVariables,
ConsumerLongitude: location.Longitude,
ConsumerLatitude: location.Latitude,
},
}
err = h.restClient.CreateJobDto(createJobDto)
if err != nil {
return nil, err
} else {
return &createJobDto.Job, nil
}
}
func (h *CommandHandler) HandleGetJobById(command ports.GetJobByIdCommand) (*ports.GetJobDto, error) {
dto, err := h.restClient.GetJob(command.ID)
if err != nil {
return nil, err
} else {
return dto, nil
}
}
func (h *CommandHandler) HandleGetAllJobs(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error) {
joblist, err := h.restClient.GetJobs()
if err != nil {
return nil, err
} else {
return joblist, nil
}
}
func (h *CommandHandler) HandleLogin(command ports.LoginCommand) (*ports.LoginResponseDto, error) {
loginDto := ports.LoginDto{
Credentials: ports.Credentials{
Username: command.UserName,
Password: command.Password,
},
}
loginResponse, err := h.restClient.Login(loginDto)
if err != nil {
return nil, err
} else {
return loginResponse, nil
}
}
func (h *CommandHandler) HandleHelp(command ports.HelpCommand) string {
return "Test"
}
func (h *CommandHandler) HandleSetConsumerLocation(command ports.SetConsumerLocationCommand) error {
err := h.locationStore.SetLocation(command.Latitude, command.Longitude)
if err != nil {
return fmt.Errorf("unable to read Location: %w", err)
}
return nil
}
func (h *CommandHandler) HandleGetConsumerLocation(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error) {
geolocation, err := h.locationStore.GetLocation()
if err != nil {
return nil, fmt.Errorf("unable to read Location: %w", err)
}
return geolocation, nil
}

View File

@ -0,0 +1,100 @@
package core
import (
"testing"
rest_client_mock "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/adapter/mocks/rest_client_mock"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
func TestCommandHandler_HandleCreateJob(t *testing.T) {
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
commandBeforeLocationSet := ports.CreateJobCommand{
JobName: "",
ImageName: "",
EnvironmentVariables: nil,
}
a, err := handler.HandleCreateJob(commandBeforeLocationSet)
if err == nil {
t.Error("Expected error, got nil ", a)
}
setLocationCmd := ports.SetConsumerLocationCommand{
Longitude: 0,
Latitude: 0,
}
err = handler.HandleSetConsumerLocation(setLocationCmd)
if err != nil {
t.Error(err)
}
commandAfterLocationSet := ports.CreateJobCommand{
JobName: "",
ImageName: "",
EnvironmentVariables: nil,
}
_, err = handler.HandleCreateJob(commandAfterLocationSet)
if err != nil {
t.Error(err)
}
jobs, err := handler.HandleGetAllJobs(ports.GetAllJobsCommand{})
if len(jobs) != 1 {
t.Error("Expected 1 job, got ", len(jobs))
}
id := jobs[0].Id
job, err := handler.HandleGetJobById(ports.GetJobByIdCommand{ID: id})
if job == nil {
t.Error("Expected job, got nil")
}
}
func TestCommandHandler_HandleLogin(t *testing.T) {
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
_, err := handler.HandleLogin(ports.LoginCommand{
UserName: "a",
Password: "b",
})
if err != nil {
t.Error(err)
}
}
func TestCommandHandler_HandleLocation(t *testing.T) {
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
_, err := handler.HandleGetConsumerLocation(ports.GetConsumerLocationCommand{})
if err == nil {
t.Error("Expected 'get location' to fail ")
}
err = handler.HandleSetConsumerLocation(ports.SetConsumerLocationCommand{
Longitude: 10,
Latitude: -7,
})
if err != nil {
t.Error(err)
}
geo, err := handler.HandleGetConsumerLocation(ports.GetConsumerLocationCommand{})
if err != nil {
t.Error(err)
}
if geo.Longitude != 10 || geo.Latitude != -7 {
t.Error("Wrong Coordinates")
}
}
func TestCommandHandler_HandleHelp(t *testing.T) {
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
help := handler.HandleHelp(ports.HelpCommand{})
if help == "" {
t.Error("Expected help")
}
}

View File

@ -0,0 +1,153 @@
package core
import (
"errors"
"regexp"
"strconv"
"strings"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type CommandParser struct {
}
const JOB_COMMAND_IDENTIFIER = "job"
const JOB_CREATE_ACTION = "create"
const JOB_GET_ACTION = "get"
const LOGIN_COMMAND_IDENTIFIER = "login"
const LOCATION_COMMAND_IDENTIFIER = "location"
const LOCATION_GET_ACTION = "get"
const LOCATION_SET_ACTION = "set"
const HELP_COMMAND_IDENTIFIER = "help"
func (c *CommandParser) ParseCommand(command string) (ports.Command, error) {
command = strings.Replace(command, "\n", "", -1)
command = strings.TrimSpace(command)
pattern := `"[^"]*"|\S+`
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(command, -1)
commandParts, err := flattenAndRemoveQuotes(matches)
if err != nil {
return nil, err
}
if len(commandParts) == 0 {
return nil, errors.New("invalid command")
}
commandIdentifier := commandParts[0]
switch commandIdentifier {
case JOB_COMMAND_IDENTIFIER:
return extractJobCommand(commandParts)
case LOGIN_COMMAND_IDENTIFIER:
return extractLoginCommand(commandParts)
case LOCATION_COMMAND_IDENTIFIER:
return extractLocationCommand(commandParts)
case HELP_COMMAND_IDENTIFIER:
return &ports.HelpCommand{}, nil
default:
return nil, errors.New("unkonwn command")
}
}
func extractLocationCommand(commandParts []string) (ports.Command, error) {
if len(commandParts) <= 1 {
return nil, errors.New("invalid location command")
}
action := commandParts[1]
switch action {
case LOCATION_GET_ACTION:
return &ports.GetConsumerLocationCommand{}, nil
case LOCATION_SET_ACTION:
if len(commandParts) != 4 {
return nil, errors.New("invalid set location command")
}
latitude, err := strconv.ParseFloat(commandParts[2], 64)
if err != nil {
return nil, errors.New("invalid latitude")
}
longitude, err := strconv.ParseFloat(commandParts[3], 64)
if err != nil {
return nil, errors.New("invalid longitude")
}
return &ports.SetConsumerLocationCommand{Longitude: longitude, Latitude: latitude}, nil
default:
return nil, errors.New("unknown location action")
}
}
func extractLoginCommand(commandParts []string) (ports.Command, error) {
if len(commandParts) != 3 {
return nil, errors.New("invalid login command")
}
return &ports.LoginCommand{UserName: commandParts[1], Password: commandParts[2]}, nil
}
func extractJobCommand(commandParts []string) (ports.Command, error) {
commandPartsLen := len(commandParts)
if commandPartsLen <= 1 {
return nil, errors.New("invalid job command")
}
action := commandParts[1]
switch action {
case JOB_CREATE_ACTION:
if commandPartsLen <= 3 {
return nil, errors.New("not enough arguments for create job command")
}
return extractCreateJobCommand(commandParts[2:])
case JOB_GET_ACTION:
if commandPartsLen == 2 {
return &ports.GetAllJobsCommand{}, nil
} else if commandPartsLen == 3 {
return &ports.GetJobByIdCommand{commandParts[2]}, nil
}
return nil, errors.New("too many arguments for get job command")
default:
return nil, errors.New("unknown job action")
}
}
func extractCreateJobCommand(args []string) (ports.Command, error) {
jobName := args[0]
imageName := args[1]
if len(args) == 2 {
return &ports.CreateJobCommand{JobName: jobName, ImageName: imageName}, nil
}
envVars := make(map[string]string)
for _, keyValuePair := range args[2:] {
parts := strings.SplitN(keyValuePair, ":", 2)
if len(parts) != 2 {
return nil, errors.New("invalid environment variable format")
}
key := parts[0]
value := parts[1]
envVars[key] = value
}
return &ports.CreateJobCommand{JobName: jobName, ImageName: imageName, EnvironmentVariables: envVars}, nil
}
func flattenAndRemoveQuotes(slices [][]string) ([]string, error) {
var result []string
for _, elements := range slices {
if len(elements) != 1 {
return nil, errors.New("something went wrong")
}
element := strings.ReplaceAll(elements[0], `"`, "")
result = append(result, element)
}
return result, nil
}

View File

@ -0,0 +1,310 @@
package core
import (
"testing"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
func TestCreateJobCommand(t *testing.T) {
commandParser := CommandParser{}
tests := []struct {
name string
command string
expectedCommand *ports.CreateJobCommand
}{
{
name: "Arguments in double quotes",
command: `job create "my test Job" "acc/myImage" "path:some/path/to/file" "user:Tony :Fallony"`,
expectedCommand: &ports.CreateJobCommand{JobName: "my test Job", ImageName: "acc/myImage", EnvironmentVariables: map[string]string{"path": "some/path/to/file", "user": "Tony :Fallony"}},
},
{
name: "Some arguments not in double quotes",
command: `job create myJob "acc/myImage" path:some/path/to/file "user:Tony Fallony"`,
expectedCommand: &ports.CreateJobCommand{JobName: "myJob", ImageName: "acc/myImage", EnvironmentVariables: map[string]string{"path": "some/path/to/file", "user": "Tony Fallony"}},
},
{
name: "No environment variables",
command: `job create myJob acc/myImage`,
expectedCommand: &ports.CreateJobCommand{JobName: "myJob", ImageName: "acc/myImage"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := commandParser.ParseCommand(tt.command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch v := result.(type) {
case *ports.CreateJobCommand:
if v.JobName != tt.expectedCommand.JobName {
t.Errorf("Wrong job name. Expected '%s' but got '%s'.", tt.expectedCommand.JobName, v.JobName)
}
if v.ImageName != tt.expectedCommand.ImageName {
t.Errorf("Wrong image name. Expected '%s' but got '%s'.", tt.expectedCommand.ImageName, v.ImageName)
}
for key, expectedValue := range tt.expectedCommand.EnvironmentVariables {
if value, ok := v.EnvironmentVariables[key]; !ok || value != expectedValue {
t.Errorf("Wrong environment variable '%s'. Expected '%s' but got '%s'.", key, expectedValue, value)
}
}
default:
t.Errorf("Create job command is of wrong type: '%T'", result)
}
})
}
}
func TestGetAllJobsCommand(t *testing.T) {
commandParser := CommandParser{}
command := `job get`
result, err := commandParser.ParseCommand(command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch result.(type) {
case *ports.GetAllJobsCommand:
if _, ok := result.(*ports.GetAllJobsCommand); !ok {
t.Errorf("Expected GetAllJobsCommand but got '%T'", result)
}
default:
t.Errorf("Get all jobs command is of wrong type: '%T'", result)
}
}
func TestGetJobByIdCommand(t *testing.T) {
commandParser := CommandParser{}
command := `job get 123`
expectedCommand := ports.GetJobByIdCommand{ID: "123"}
result, err := commandParser.ParseCommand(command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch v := result.(type) {
case *ports.GetJobByIdCommand:
if v.ID != expectedCommand.ID {
t.Errorf("Wrong job ID. Expected '%s' but got '%s'.", expectedCommand.ID, v.ID)
}
default:
t.Errorf("Get job by ID command is of wrong type: '%T'", result)
}
}
func TestLoginCommand(t *testing.T) {
commandParser := CommandParser{}
tests := []struct {
name string
command string
expectedCommand ports.LoginCommand
}{
{
name: "Arguments in double quotes",
command: `login "username" "my secret password"`,
expectedCommand: ports.LoginCommand{UserName: "username", Password: "my secret password"},
},
{
name: "Some arguments not in double quotes",
command: `login userName "password"`,
expectedCommand: ports.LoginCommand{UserName: "userName", Password: "password"},
},
{
name: "All arguments not in double quotes",
command: `login username password`,
expectedCommand: ports.LoginCommand{UserName: "username", Password: "password"},
},
{
name: "Login with empty password",
command: `login "username" ""`,
expectedCommand: ports.LoginCommand{UserName: "username", Password: ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := commandParser.ParseCommand(tt.command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch v := result.(type) {
case *ports.LoginCommand:
if v.UserName != tt.expectedCommand.UserName {
t.Errorf("Wrong username. Expected '%s' but got '%s'.", tt.expectedCommand.UserName, v.UserName)
}
if v.Password != tt.expectedCommand.Password {
t.Errorf("Wrong password. Expected '%s' but got '%s'.", tt.expectedCommand.Password, v.Password)
}
default:
t.Errorf("Login command is of wrong type: '%T'", result)
}
})
}
}
func TestGetConsumerLocationCommand(t *testing.T) {
commandParser := CommandParser{}
command := `location get`
result, err := commandParser.ParseCommand(command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch result.(type) {
case *ports.GetConsumerLocationCommand:
if _, ok := result.(*ports.GetConsumerLocationCommand); !ok {
t.Errorf("Expected GetConsumerLocationCommand but got '%T'", result)
}
default:
t.Errorf("Get consumer location command is of wrong type: '%T'", result)
}
}
func TestSetConsumerLocationCommand(t *testing.T) {
commandParser := CommandParser{}
tests := []struct {
name string
command string
expectedCommand ports.SetConsumerLocationCommand
}{
{
name: "Arguments with quotes",
command: `location set "-122.4194" "37.7749"`,
expectedCommand: ports.SetConsumerLocationCommand{Longitude: 37.7749, Latitude: -122.4194},
},
{
name: "Some arguments with quotes",
command: `location set "123" 0 `,
expectedCommand: ports.SetConsumerLocationCommand{Longitude: 0, Latitude: 123},
},
{
name: "No arguments with quotes",
command: `location set -122.4194 -37.7749`,
expectedCommand: ports.SetConsumerLocationCommand{Longitude: -37.7749, Latitude: -122.4194},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := commandParser.ParseCommand(tt.command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch v := result.(type) {
case *ports.SetConsumerLocationCommand:
if v.Longitude != tt.expectedCommand.Longitude {
t.Errorf("Wrong longitude. Expected '%f' but got '%f'.", tt.expectedCommand.Longitude, v.Longitude)
}
if v.Latitude != tt.expectedCommand.Latitude {
t.Errorf("Wrong latitude. Expected '%f' but got '%f'.", tt.expectedCommand.Latitude, v.Latitude)
}
default:
t.Errorf("Set consumer location command is of wrong type: '%T'", result)
}
})
}
}
func TestHelpCommand(t *testing.T) {
commandParser := CommandParser{}
command := `help`
result, err := commandParser.ParseCommand(command)
if err != nil {
t.Errorf("Error: %s", err)
}
switch result.(type) {
case *ports.HelpCommand:
if _, ok := result.(*ports.HelpCommand); !ok {
t.Errorf("Expected HelpCommand but got '%T'", result)
}
default:
t.Errorf("Help command is of wrong type: '%T'", result)
}
}
func TestInvalidCommands(t *testing.T) {
commandParser := CommandParser{}
tests := []struct {
name string
command string
}{
{
name: "Empty command",
command: ``,
},
{
name: "Unknown command",
command: `unknown command`,
},
{
name: "Job command with invalid action",
command: `job unknown`,
},
{
name: "Create job command with missing args",
command: `job create`,
},
{
name: "Create job command with missing image name",
command: `job create myJob`,
},
{
name: "Create job command with invalid envs",
command: `job create myJob myImage "key1"`,
},
{
name: "Invalid job get command with too many arguments",
command: `job get 123 extra`,
},
{
name: "Invalid location action",
command: `location unknown`,
},
{
name: "Invalid location set command with missing arguments",
command: `location set 37.7749`,
},
{
name: "Invalid location set command with non numeric longitude",
command: `location set longi 37.7749`,
},
{
name: "Invalid location set command with non numeric latitude",
command: `location set 37.7749 lati`,
},
{
name: "Invalid login command with missing arguments",
command: `login`,
},
{
name: "Invalid login command with missing password",
command: `login username`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := commandParser.ParseCommand(tt.command)
if err == nil {
t.Errorf("Expected error for command '%s' but got none", tt.command)
}
})
}
}

View File

@ -0,0 +1,83 @@
package location_store
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
type locationStore struct {
}
type LocationStore interface {
GetLocation() (*ports.Geolocation, error)
SetLocation(latitude float64, longitude float64) error
}
func NewLocationStore() *locationStore {
return &locationStore{}
}
const filename = ".userlocation"
func (store *locationStore) GetLocation() (*ports.Geolocation, error) {
geo, err := readFile()
if err != nil {
return nil, err
}
return geo, nil
}
func (store *locationStore) SetLocation(latitude float64, longitude float64) error {
geo := ports.Geolocation{
Latitude: latitude,
Longitude: longitude,
}
err := writeToFile(geo)
return err
}
func readFile() (*ports.Geolocation, error) {
exePath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("error finding executable path: %w", err)
}
configDir := filepath.Dir(exePath)
configFilePath := filepath.Join(configDir, filename)
file, err := os.Open(configFilePath)
if err != nil {
return nil, fmt.Errorf("location was not set")
}
defer file.Close()
location := &ports.Geolocation{}
decoder := json.NewDecoder(file)
if err := decoder.Decode(location); err != nil {
return nil, fmt.Errorf("error decoding file: %w", err)
}
return location, nil
}
func writeToFile(location ports.Geolocation) error {
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("error finding executable path: %w", err)
}
configDir := filepath.Dir(exePath)
configFilePath := filepath.Join(configDir, filename)
file, err := os.Create(configFilePath)
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
if err := encoder.Encode(location); err != nil {
return fmt.Errorf("error encoding file: %w", err)
}
return nil
}

View File

@ -0,0 +1,61 @@
package location_store
import (
"encoding/json"
_ "fmt"
"os"
"path/filepath"
"reflect"
"testing"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
)
func TestSetLocation(t *testing.T) {
store := NewLocationStore()
err := store.SetLocation(40.7128, -74.0060)
if err != nil {
t.Errorf("Error setting location: %v", err)
}
exePath, _ := os.Executable()
configFilePath := filepath.Join(filepath.Dir(exePath), filename)
t.Log(configFilePath)
file, err := os.Open(configFilePath)
if err != nil {
t.Fatalf("Error opening config file: %v", err)
}
defer file.Close()
var savedLocation ports.Geolocation
decoder := json.NewDecoder(file)
if err := decoder.Decode(&savedLocation); err != nil {
t.Fatalf("Error decoding saved location: %v", err)
}
expectedLocation := ports.Geolocation{Latitude: 40.7128, Longitude: -74.0060}
if savedLocation != expectedLocation {
t.Errorf("Expected %v, got %v", expectedLocation, savedLocation)
}
}
func TestGetLocation(t *testing.T) {
mockLocation := ports.Geolocation{Latitude: 51.5074, Longitude: -0.1278}
store := NewLocationStore()
err := store.SetLocation(mockLocation.Latitude, mockLocation.Longitude)
if err != nil {
t.Errorf("Error setting location: %v", err)
}
location, err := store.GetLocation()
if err != nil {
t.Errorf("Error getting location: %v", err)
}
if reflect.DeepEqual(location, mockLocation) {
t.Errorf("Expected %v, got %v", mockLocation, *location)
}
}

View File

@ -0,0 +1,5 @@
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli
go 1.23.1
require github.com/google/uuid v1.6.0

View File

@ -0,0 +1,2 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View File

@ -0,0 +1,25 @@
package main
import (
"bufio"
"fmt"
"os"
rest_client_mock "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/adapter/mocks/rest_client_mock"
cli "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/core/cli"
location_store "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/core/location-store"
)
func main() {
restClient := rest_client_mock.NewMockRestClient()
locationStore := location_store.NewLocationStore()
commandHandler := cli.NewCommandHandler(restClient, locationStore)
cli := cli.NewCli(commandHandler)
for true {
fmt.Print("Input: ")
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
cli.Scan(input)
}
}

View File

@ -0,0 +1,56 @@
package ports
type Api interface {
HandleCreateJob(command CreateJobCommand) (*BaseJob, error)
HandleGetJobById(command GetJobByIdCommand) (*GetJobDto, error)
HandleGetAllJobs(command GetAllJobsCommand) ([]GetJobDto, error)
HandleLogin(command LoginCommand) (*LoginResponseDto, error)
HandleHelp(command HelpCommand) string
HandleSetConsumerLocation(command SetConsumerLocationCommand) error
HandleGetConsumerLocation(command GetConsumerLocationCommand) (*Geolocation, error)
}
type Command interface {
implementCommand()
}
type CreateJobCommand struct {
JobName string
ImageName string
EnvironmentVariables map[string]string
}
type GetAllJobsCommand struct{}
type GetJobByIdCommand struct {
ID string
}
type LoginCommand struct {
UserName string
Password string
}
type GetConsumerLocationCommand struct {
}
type SetConsumerLocationCommand struct {
Longitude float64
Latitude float64
}
type HelpCommand struct{}
func (c *CreateJobCommand) implementCommand() {}
func (c *GetAllJobsCommand) implementCommand() {}
func (c *GetJobByIdCommand) implementCommand() {}
func (c *LoginCommand) implementCommand() {}
func (c *GetConsumerLocationCommand) implementCommand() {}
func (c *SetConsumerLocationCommand) implementCommand() {}
func (c *HelpCommand) implementCommand() {}

View File

@ -0,0 +1,7 @@
package ports
type JWTStore interface {
GetJWT() string
SetJWT(jwt string)
DeleteJWT()
}

View File

@ -0,0 +1,63 @@
package ports
type GetJobDto struct {
Id string `json:"id"`
Name string `json:"name"`
ConsumerId string `json:"consumerId"`
ImageName string `json:"imageName"`
EnvironmentVariables map[string]string `json:"environmentVariables"`
Status JobStatus `json:"status"`
StandardOutput string `json:"standardOutput"`
CreatedAt int64 `json:"createdAt"`
StartedAt int64 `json:"startedAt"`
FinishedAt int64 `json:"finishedAt"`
ConsumerLongitude float64 `json:"consumerLongitude"`
ConsumerLatitude float64 `json:"consumerLatitude"`
Co2EquivalentEmissionConsumer float64 `json:"co2EquivalentEmissionConsumer"`
EstimatedCo2Equivalent float64 `json:"estimatedCo2Equivalent"`
}
type JobStatus string
const (
CREATED JobStatus = "CREATED" // job created, but not assigned to worker yet
PENDING JobStatus = "PENDING" // assigned to worker, but not running yet
RUNNING JobStatus = "RUNNING"
FINISHED JobStatus = "FINISHED"
FAILED JobStatus = "FAILED"
)
type CreateJobDto struct {
RequestId string `json:"requestId"`
Job BaseJob `json:"job"`
}
type LoginDto struct {
RequestId string `json:"requestId"`
Credentials Credentials `json:"credentials"`
}
type LoginResponseDto struct {
Token string `json:"token"`
}
type BaseJob struct {
Name string `json:"name"`
ImageName string `json:"imageName"`
EnvironmentVariables map[string]string `json:"environmentVariables"`
ConsumerLongitude float64 `json:"consumerLongitude"`
ConsumerLatitude float64 `json:"consumerLatitude"`
}
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Geolocation struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}

View File

@ -0,0 +1,14 @@
package ports
import (
"errors"
)
var ErrEntityNotFound = errors.New("entity not found")
type RestClient interface {
GetJob(id string) (*GetJobDto, error)
GetJobs() ([]GetJobDto, error)
CreateJobDto(dto CreateJobDto) error
Login(dto LoginDto) (*LoginResponseDto, error)
}

View File

@ -0,0 +1,33 @@
package cmd
import (
"fmt"
"os"
)
type Command struct {
Use string
Description string
Run func([]string)
}
var commands = make(map[string]Command)
func Execute() {
if len(os.Args) < 2 {
fmt.Println("Not enough arguments!")
os.Exit(1)
}
command, ok := commands[os.Args[1]]
if !ok {
fmt.Println("Unknown command!")
os.Exit(1)
}
command.Run(os.Args[2:])
}
func RegisterCommand(command Command) {
commands[command.Use] = command
}

View File

@ -0,0 +1,22 @@
package cmd
import (
"fmt"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/core"
)
func init() {
RegisterCommand(Command{
Use: "gcls-modulith",
Description: "A container image is passed, turned into a job and then executed.",
Run: func(args []string) {
if len(args) != 1 {
fmt.Println("Invalid number of arguments!")
return
}
core.ExecuteComputeJob(args[0])
},
})
}

View File

@ -0,0 +1,83 @@
package core
import (
"context"
"fmt"
"time"
worker_client "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/services/gcls-worker-daemon/adapters/client-cli"
worker_executor "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/services/gcls-worker-daemon/adapters/executor-cli"
worker_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/core"
worker_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/ports"
worker_repo "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry/adapters/repo-in-memory"
worker_registry_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry/core"
job_repo "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/adapters/repo-in-memory"
job_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/core"
job_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/ports"
// job_scheduler_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job-scheduler/core"
)
func ExecuteComputeJob(dockerImage string) {
workerRegistryService := worker_registry_core.NewWorkerRegistryService(worker_repo.NewRepo())
jobService := job_core.NewJobService(job_repo.NewRepo())
// jobSchedulerService := job_scheduler_core.NewJobSchedulerService(nil, nil)
workerDaemon := worker_core.NewWorkerDaemon(
worker_client.NewClient(workerRegistryService, jobService),
worker_executor.NewExecutor(),
&worker_ports.Config{
Longitude: 1.0,
Latitude: 1.0,
GatewayUrl: "don't care",
},
)
ctx, cancel := context.WithCancel(context.Background())
workerDone := make(chan struct{})
// schedulerDone := make(chan struct{})
go workerDaemon.Start(ctx, workerDone)
// go jobSchedulerService.Schedule(ctx, schedulerDone)
jobService.CreateJob(
context.Background(),
job_ports.CreateJobParams{
Name: "A cool job",
ImageName: "A cool image",
ConsumerLongitude: 1.0,
ConsumerLatitude: 1.0,
EnvironmentVariables: make(map[string]string),
},
"Gandalf",
)
for {
jobs, err := jobService.GetJobsForConsumer(context.Background(), "Gandalf")
if err != nil {
fmt.Printf("Encountered error while polling jobs for Gandalf: %v\n", err)
break
}
if len(jobs) != 1 {
fmt.Println("There should exactly be one job from Gandalf")
break
}
job := jobs[0]
if job.Status == job_ports.FINISHED || job.Status == job_ports.FAILED {
break
}
fmt.Println("Waiting for Job to be done.")
time.Sleep(3 * time.Second)
}
cancel()
<-workerDone
// <-schedulerDone
}

View File

@ -0,0 +1,19 @@
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith
go 1.23.2
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon => ../../services/gcls-worker-daemon
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job => ../../services/job
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry => ../../services/worker-registry
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job-scheduler => ../../services/job-scheduler
require (
gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon v0.0.0-00010101000000-000000000000
gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job v0.0.0-00010101000000-000000000000
gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry v0.0.0-00010101000000-000000000000
)
require github.com/google/uuid v1.6.0 // indirect

View File

@ -0,0 +1,2 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View File

@ -0,0 +1,7 @@
package main
import "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/cmd"
func main() {
cmd.Execute()
}

View File

@ -0,0 +1,75 @@
package client_cli
import (
"context"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/ports"
job_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/ports"
worker_registry_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry/ports"
)
type Client struct {
WorkerRegistryService worker_registry_ports.Api
JobService job_ports.Api
}
var _ ports.Client = (*Client)(nil)
func NewClient(workerRegistryService worker_registry_ports.Api, jobService job_ports.Api) *Client {
return &Client{WorkerRegistryService: workerRegistryService, JobService: jobService}
}
func (c *Client) Register(worker ports.Worker) error {
return c.WorkerRegistryService.RegisterWorker(
worker_registry_ports.Worker{
Id: worker.Id,
Status: worker_registry_ports.WorkerStatus(worker.Status),
Location: worker_registry_ports.Location{
Latitude: worker.Location.Latitude,
Longitude: worker.Location.Longitude,
},
},
context.Background())
}
func (c *Client) Unregister(id string) error {
return c.WorkerRegistryService.UnregisterWorker(id, context.Background())
}
func (c *Client) GetJobs(id string) ([]ports.Job, error) {
jobs, err := c.JobService.GetJobsForWorker(context.Background(), id)
if err != nil {
return nil, err
}
workerJobs := make([]ports.Job, len(jobs))
for i, job := range jobs {
workerJobs[i] = ports.Job{
Id: job.Id,
ImageName: job.ImageName,
EnvironmentVariables: job.EnvironmentVariables,
Status: ports.JobStatus(job.Status),
StartedAt: job.StartedAt,
FinishedAt: job.FinishedAt,
}
}
return workerJobs, nil
}
func (c *Client) UpdateStatus(id string, status ports.WorkerStatus) error {
return c.WorkerRegistryService.UpdateWorker(id, worker_registry_ports.WorkerStatus(status), context.Background())
}
func (c *Client) UpdateJob(id string, stdout string, startedAt int64, finishedAt int64, status ports.JobStatus) error {
return c.JobService.UpdateJobForWorker(
context.Background(),
id,
job_ports.UpdateJobForWorkerParams{
StandardOutput: stdout,
StartedAt: startedAt,
FinishedAt: finishedAt,
Status: job_ports.JobStatus(status),
},
)
}

View File

@ -0,0 +1,20 @@
package executor_cli
import "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/ports"
type Executor struct{}
var _ ports.Executor = (*Executor)(nil)
func NewExecutor() *Executor {
return &Executor{}
}
func (c *Executor) Execute(job ports.Job) (ports.ExecutionResult, error) {
return ports.ExecutionResult{
Status: ports.SUCCESS,
StartedAt: 0,
FinishedAt: 0,
Stdout: "Hello, World!",
}, nil
}

190
doc/README.md 100644
View File

@ -0,0 +1,190 @@
# Green Compute Load Shifting (GCLS)
## Use Cases
![use cases](use-cases.png)
## Building Blocks
![building blocks](building-blocks.png)
| Building Block | Type | Purpose | Owner |
| ------ | --- | -------------- | --- |
| GCLS Worker Daemon | CLI | Provide computing capacity to platform; Execution of jobs | Team 1 |
| Worker Gateway | Microservice | Provides unified API for the GCLS Worker Daemon | Team 1 |
| Worker Registry | Microservice | Holds Status of Worker Daemons | Team 1 |
| GCLS Consumer CLI | CLI | Place compute jobs; Receive Job Status/Results | Team 2 |
| Consumer Gateway | Microservice | Provides unified API for the GCLS Consumer CLI | Team 2 |
| Job | Microservice | Manages the status of Jobs | Team 3 |
| User Management | Microservice | Manages Users | Team 3 |
| Job Scheduler | Microservice | Schedules Jobs | Team 4 |
| Carbon Intensity Provider | Microservice | Provides Carbon Intensity | Team 4 |
## Approved Packages
- [google/uuid](https://pkg.go.dev/github.com/google/uuid)
- [opentelemetry](https://pkg.go.dev/go.opentelemetry.io/otel)
## Runtime Scenarios
### Run Compute Job
```mermaid
sequenceDiagram
participant worker as GCLS Worker Daemon
participant gateway as Worker Gateway
participant registry as Worker Registry
participant job as Job
loop every 5s
worker->>gateway: UpdateStatus(worker)
activate gateway
gateway->>registry: UpdateStatus(worker)
activate registry
registry-->>gateway: 200 OK
deactivate registry
gateway-->>worker: 200 OK
deactivate gateway
end
loop
worker->>gateway: GetJob(worker)
activate gateway
gateway->>job: GetJob(worker)
activate job
job-->>gateway: Job
deactivate job
gateway-->>worker: Job
deactivate gateway
worker->>worker: SetStatus("EXHAUSTED")
worker->>worker: Execute(job)
worker->>gateway: Update(job)
activate gateway
gateway->>job: Update(job)
activate job
job-->>gateway: 200 OK
deactivate job
gateway-->>worker: 200 OK
deactivate gateway
worker->>worker: SetStatus("REQUIRES_WORK")
end
```
### Create Compute Job
```mermaid create_job
sequenceDiagram
actor ComputeConsumer as Compute Consumer
ComputeConsumer ->> GCLS_CLI : Job anlegen mit Job-Definition
note right of GCLS_CLI: Annahme: Der User ist bereits registriert
GCLS_CLI ->> GCLS_CLI : JWT aus Speicher lesen
alt Gültiger JWT vorhanden
GCLS_CLI ->> ConsumerGateway : POST /jobs (+ JWT)
ConsumerGateway ->> ConsumerGateway : Validiere JWT
alt JWT gültig
ConsumerGateway ->> Job : POST /jobs (+ JWT)
alt Job und JWT gültig
Job -->> ConsumerGateway : Job gültig und angelegt
ConsumerGateway -->> GCLS_CLI : Job gültig und angelegt
GCLS_CLI -->> ComputeConsumer : Job erfolgreich angelegt
else Job ungültig
Job -->> ConsumerGateway : Job ungültig
ConsumerGateway -->> GCLS_CLI : Job ungültig
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "ungültiger Job"
else JWT ungültig
Job -->> ConsumerGateway : JWT ungültig
ConsumerGateway -->> GCLS_CLI : JWT ungültig
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "nicht authentifiziert"
end
else JWT ungültig
ConsumerGateway -->> GCLS_CLI : JWT ungültig
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "nicht authentifiziert"
end
else JWT nicht vorhanden oder abgelaufen
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "nicht authentifiziert"
end
```
### Login
```mermaid login
sequenceDiagram
actor ComputeConsumer as Compute Consumer
ComputeConsumer ->> GCLS_CLI: Anmelden mit Nutzername + Passwort
GCLS_CLI ->> ConsumerGateway: POST /login
ConsumerGateway ->> UserManagement: POST /login
alt Zugangsdaten gültig und Anfrage authentifiziert
UserManagement -->> ConsumerGateway: JWT
ConsumerGateway -->> GCLS_CLI: JWT
GCLS_CLI ->> GCLS_CLI: JWT sicher speichern
GCLS_CLI -->> ComputeConsumer: Erfolgreich angemeldet
else Zugangsdaten ungültig
UserManagement -->> ConsumerGateway: ungültige Zugangsdaten
ConsumerGateway -->> GCLS_CLI: ungültige Zugangsdaten
GCLS_CLI -->> ComputeConsumer: Fehlernachricht "ungültige Zugangsdaten"
else Anfrage (über basic auth) nicht authentifiziert
UserManagement -->> ConsumerGateway: Anfrage über basic auth nicht authentifiziert
ConsumerGateway -->> GCLS_CLI: interner Fehler
GCLS_CLI -->> ComputeConsumer: Fehlernachricht "interner Fehler"
end
```
### Get Job Statistics
```mermaid
sequenceDiagram
actor Consumer
note right of Consumer: Annahme: Der User ist bereits eingeloggt
note right of GCLS_CLI: Annahme: Dauerhafte Netzwerk- und Endpoint-Erreichbarkeit
Consumer ->> GCLS_CLI: Request Job statistics
alt Gültiger JWT vorhanden
GCLS_CLI ->> Consumer-Gateway : POST /jobs (+ JWT)
Consumer-Gateway ->> Consumer-Gateway : Validiere JWT
alt JWT gültig
Consumer-Gateway ->> Job : POST /jobs (+ JWT)
alt Job und JWT gültig
Job -->> Job: Retrieve statistics from database
alt Database success
Job -->> Consumer-Gateway : Return job statistics
Consumer-Gateway -->> GCLS_CLI : Return job statistics
GCLS_CLI -->> Consumer : Return job statistics
else Database error
Job -->> Consumer-Gateway : Retrieving failed
Consumer-Gateway -->> GCLS_CLI : Retrieving failed
GCLS_CLI -->> Consumer : Fehlernachricht "Datenbankfehler"
end
else JWT ungültig
Job -->> Consumer-Gateway : JWT ungültig
Consumer-Gateway -->> GCLS_CLI : JWT ungültig
GCLS_CLI -->> Consumer : Fehlernachricht "nicht authentifiziert"
end
else JWT ungültig vom Consumer-Gateway
Consumer-Gateway -->> GCLS_CLI : JWT ungültig
GCLS_CLI -->> Consumer : Fehlernachricht "nicht authentifiziert"
end
end
```
### Schedule Job
![jobs-worker-cip](jobs-worker-cip.jpeg)
## Cross Cutting Concerns
| Aspect | Owner |
| --- | --- |
| Observability | Team 1 |
| DevOps | Team 2 |
| Security | Team 3 |
| Communication Resilience | Team 4 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -0,0 +1,334 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.8 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36" version="24.7.8" pages="3">
<diagram name="Overview" id="9SfsONUl5Bj8uXdVnY35">
<mxGraphModel dx="4634" dy="1196" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="bkjyaAWGgHkUM0_QegR--12" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;verticalAlign=bottom;" parent="1" vertex="1">
<mxGeometry x="79.99999999999997" y="110" width="120" height="120" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--1" value="" style="html=1;verticalLabelPosition=bottom;align=center;labelBackgroundColor=#ffffff;verticalAlign=top;strokeWidth=2;strokeColor=#0080F0;shadow=0;dashed=0;shape=mxgraph.ios7.icons.cloud;" parent="1" vertex="1">
<mxGeometry x="236.22" y="70" width="530" height="170" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="bkjyaAWGgHkUM0_QegR--3" target="bkjyaAWGgHkUM0_QegR--7" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--17" value="get current carbon intensity" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="bkjyaAWGgHkUM0_QegR--8" vertex="1" connectable="0">
<mxGeometry x="-0.1849" y="-2" relative="1" as="geometry">
<mxPoint x="4" y="8" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--3" value="Green Compute Load Shifting (GCLS) Platform" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="366.22" y="150" width="260" height="60" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--4" value="Electricity Maps" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;verticalAlign=bottom;" parent="1" vertex="1">
<mxGeometry x="816.22" y="120" width="120" height="120" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--7" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=data:image/png,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAaVBMVEX///8AAADx8fG8vLxDQ0OFhYX6+vrW1tYjIyP29vaBgYFVVVXl5eWXl5dRUVHCwsIWFhYaGhqWlpYODg7Ozs7r6+szMzNvb2+np6e2traOjo6fn5+tra1fX187Ozvg4OArKytpaWl3d3cBzJlsAAAD0klEQVR4nO3diWKiMBAGYNN6Ua1HrVp72fb9H3K36wGEJJAwOMPs/z3ANL9WJAMjgwEAAAAAAAAAAAAAAAAAQIzJaDgdjibq6lxkM3MyGxPVyUTUuZqb3FxRndyqUHHaos66UGfdos60UGfVos7Vgyl6UFPnamzKUj+K0urk7q2Ky8Q6S2F1ck9WxbfEOo9WnUfmOrmRVXGkpE5O/3uo/3Mo7RhIfyy1vn/S/+2p6pQ/iATfh+UXbZP+kpVf/BZ1NsRvobzzSfrz0kG2ONdruye41FkQ7S1a1imStq+j3h8CAAAAAAAAQLztlnsF3frt1Xyqzrj77SCRXNcUan9qkuntIb2c24D33AvpysQoT5gZ7QmH2hMWLj7pTHgwyhMejfKE45X2hDOjPOGrUZ7QvoFCXcKt0Z7wS3vCDzugtoRvlYDKEj5XA+pKeOcIqCvhp/aEe1dATQnfnQEVJZy4A+pJmHkC6kk41J7QvulcXUL7DnF1Cef+gDoSjtfaE84CAVUktAdR1CW02xaWQ3ZXhztBjUrbIt6m/TBMlyptixTP3CkCvikCmm/uGH6OtkWK3S3XHDXf4NtQxAqMNPHO4zvbFim8h9Mx8zy+s22RwHug4Z7H/yEK+OT9C8zz+J62RbQPovU0EDezeTQ0/O8N8zy+/eeT+T9fzPP4C0MjcGbOO4/vb1vEifkbN53HD7QtoixC6+Gcxw+1LaIEv8c55/FXhsaRaD2NNZ2jr14HTfNesx62efxw26K5n9r1MM3ju66DpmiwZeKZxydoW5w06c+wzONTnW837FwwzOPvaAKm/vjNDdAcSAV3ZmgONG1+DK57y9BlioakD5rUdrGzmt2x7BZwI+Ezc3/boj+CCYMbir4IJVyTnH5xCyUMbyj6IpDwwL02Gv6E7VsRMngTDrlXRsWbkG6LwMyXUM+8pSdhXduiR9wJ99zLIuRM+Mm9KkrOhNJvK4niSij5hot4joSC2xYpqgn910H7qZLwi3tF1CoJpbctolH9rLRcVsJX7vXQKyeccS+nA6WEKxVtC0spoY62haWYUEnbwlJISHHpVqA8oZq2hSVPqKZtYbkm1NO2sFwSvnAvpDPni4ya2ha2fwFveoP6rW13fw+jqtoWVdp/NBEAAAAAAABAOGnPLeSdx29Sh+gZlkzz+L2pk4ubx/crziusW9RhnsfvT50rac9iZp7HD5D7XG79z1aX9lx71nn8ILnvof7PobRjIOM8/q3qsM3jx9Vp8dJzzeP3sE5Bg3n8uDot5+hZ5vF7WgcAAAAAAAAAAAAAAAAA/i9/AOyoKhG6kvueAAAAAElFTkSuQmCC;" parent="1" vertex="1">
<mxGeometry x="836.22" y="140" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--10" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=https://pages.okta.com/rs/855-QAH-699/images/email-main-template_auth0-by-okta-logo_black_279x127_3x.png;" parent="1" vertex="1">
<mxGeometry x="85.08" y="150" width="109.84" height="50" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.997;entryY=0.582;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryPerimeter=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--3" target="bkjyaAWGgHkUM0_QegR--12" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--16" value="authentication" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="bkjyaAWGgHkUM0_QegR--14" vertex="1" connectable="0">
<mxGeometry x="-0.3806" y="1" relative="1" as="geometry">
<mxPoint x="-24" y="9" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--15" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="261.22" y="270" />
<mxPoint x="431.22" y="270" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--19" value="Get Jobs&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="bkjyaAWGgHkUM0_QegR--18" vertex="1" connectable="0">
<mxGeometry x="-0.241" y="2" relative="1" as="geometry">
<mxPoint x="70" y="-8" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--37" value="" style="group" parent="1" vertex="1" connectable="0">
<mxGeometry x="216.22" y="290" width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--15" value="GCLS Worker" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--37" vertex="1">
<mxGeometry width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--2" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--37" vertex="1">
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--38" value="" style="group" parent="1" vertex="1" connectable="0">
<mxGeometry x="316.22" y="290" width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--39" value="GCLS Worker" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--38" vertex="1">
<mxGeometry width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--40" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--38" vertex="1">
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--41" value="" style="group" parent="1" vertex="1" connectable="0">
<mxGeometry x="416.22" y="290" width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--42" value="GCLS Worker" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--41" vertex="1">
<mxGeometry width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--43" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--41" vertex="1">
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--39" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="361.22" y="270" />
<mxPoint x="431.22" y="270" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--42" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="461.22" y="270" />
<mxPoint x="431.22" y="270" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--46" value="" style="group" parent="1" vertex="1" connectable="0">
<mxGeometry x="606.22" y="290" width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--47" value="GCLS CLI" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--46" vertex="1">
<mxGeometry width="90" height="79" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--48" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--46" vertex="1">
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--49" value="&lt;font style=&quot;font-size: 10px;&quot;&gt;Compute Provider&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
<mxGeometry x="246.22" y="390" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--56" value="&lt;font style=&quot;font-size: 10px;&quot;&gt;Compute Provider&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
<mxGeometry x="346.22" y="390" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--57" value="&lt;font style=&quot;font-size: 10px;&quot;&gt;Compute Provider&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
<mxGeometry x="446.22" y="390" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--58" value="&lt;font style=&quot;font-size: 10px;&quot;&gt;Compute Consumer&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
<mxGeometry x="636.22" y="390" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="bkjyaAWGgHkUM0_QegR--59" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--47" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="651.22" y="270" />
<mxPoint x="431.22" y="270" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="bpHMyEXNbauoO1gqHrBm" name="Use Cases">
<mxGraphModel dx="1184" dy="678" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Tsc4UfA5dBkjLhyos3iL-1" value="Green Compute Load Shifter" style="swimlane;whiteSpace=wrap;html=1;startSize=23;" parent="1" vertex="1">
<mxGeometry x="240" y="90" width="300" height="420" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-4" value="run compute job" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
<mxGeometry x="10" y="330" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-5" value="create compute job" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
<mxGeometry x="90" y="25" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-6" value="get compute job results" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
<mxGeometry x="33" y="90" width="89" height="57" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-7" value="get job statistics" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
<mxGeometry x="140" y="220" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-17" value="monitor system" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
<mxGeometry x="175" y="300" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-19" value="d&lt;span style=&quot;background-color: initial;&quot;&gt;eploy new version&lt;/span&gt;" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
<mxGeometry x="150" y="100" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="Qs-BkcHP9t_8JN-s5nUO-1" value="login" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="Tsc4UfA5dBkjLhyos3iL-1">
<mxGeometry x="2" y="192" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="OeV4-BEXwWvXPAKTQ9E--1" value="Compute Consumer" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="170" y="170" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-3" value="Compute Provider" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="170" y="360" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-8" value="Operations" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="580" y="270" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-11" value="" style="endArrow=none;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-5" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="210" y="170" as="sourcePoint" />
<mxPoint x="270" y="200" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-12" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="210" y="200" as="sourcePoint" />
<mxPoint x="270" y="200" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-13" value="" style="endArrow=none;html=1;rounded=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-4" edge="1" source="yRPADHfy2BcxKq2ry5dm-3">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="220" y="310" as="sourcePoint" />
<mxPoint x="340" y="320" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-15" value="Developer" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="580" y="130" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-21" value="" style="endArrow=none;html=1;rounded=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-19" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="160" as="sourcePoint" />
<mxPoint x="278" y="232" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-22" value="" style="endArrow=none;html=1;rounded=0;entryX=1.025;entryY=0.425;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-7" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="290" as="sourcePoint" />
<mxPoint x="502" y="212" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="yRPADHfy2BcxKq2ry5dm-23" value="" style="endArrow=none;html=1;rounded=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-17" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="320" as="sourcePoint" />
<mxPoint x="463" y="324" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Qs-BkcHP9t_8JN-s5nUO-2" value="" style="endArrow=none;html=1;rounded=0;entryX=0.081;entryY=0.229;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="Qs-BkcHP9t_8JN-s5nUO-1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="210" y="230" as="sourcePoint" />
<mxPoint x="278" y="222" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Qs-BkcHP9t_8JN-s5nUO-3" value="" style="endArrow=none;html=1;rounded=0;exitX=0.028;exitY=0.671;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="Qs-BkcHP9t_8JN-s5nUO-1" target="yRPADHfy2BcxKq2ry5dm-3">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="220" y="200" as="sourcePoint" />
<mxPoint x="210" y="370" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="OxRlZlBOkbXn22t2wwnC-1" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="210" y="220" as="sourcePoint" />
<mxPoint x="410" y="310" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="sQFYLQE7cxVQcqrETYkd" name="Building Blocks">
<mxGraphModel dx="4634" dy="1196" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="LCirkKGBkMxsQzFvBwtC-2" value="Legend" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;" parent="1" vertex="1">
<mxGeometry x="830" y="60" width="120" height="170" as="geometry" />
</mxCell>
<mxCell id="-GJQ50JBE78Y6x4tCiOK-1" value="&lt;span style=&quot;color: rgb(0, 0, 0);&quot;&gt;Green Compute Load Shifting (GCLS) Platform&lt;/span&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="60" y="60" width="550" height="370" as="geometry" />
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="Y_95Ov9H_2BIZraajzK3-1" target="APDtbEWumEfnyccvbBP0-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="Y_95Ov9H_2BIZraajzK3-1" target="w-Rq3PLQNwYjt5HaCK5d-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="Y_95Ov9H_2BIZraajzK3-1" target="w-Rq3PLQNwYjt5HaCK5d-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Y_95Ov9H_2BIZraajzK3-1" value="Job Scheduler" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="280" y="100" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="sKb6c6YfNXrpK8ux16pL-1" value="Electricity Maps" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;verticalAlign=bottom;" parent="1" vertex="1">
<mxGeometry x="650" y="80" width="120" height="120" as="geometry" />
</mxCell>
<mxCell id="sKb6c6YfNXrpK8ux16pL-2" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=data:image/png,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAaVBMVEX///8AAADx8fG8vLxDQ0OFhYX6+vrW1tYjIyP29vaBgYFVVVXl5eWXl5dRUVHCwsIWFhYaGhqWlpYODg7Ozs7r6+szMzNvb2+np6e2traOjo6fn5+tra1fX187Ozvg4OArKytpaWl3d3cBzJlsAAAD0klEQVR4nO3diWKiMBAGYNN6Ua1HrVp72fb9H3K36wGEJJAwOMPs/z3ANL9WJAMjgwEAAAAAAAAAAAAAAAAAQIzJaDgdjibq6lxkM3MyGxPVyUTUuZqb3FxRndyqUHHaos66UGfdos60UGfVos7Vgyl6UFPnamzKUj+K0urk7q2Ky8Q6S2F1ck9WxbfEOo9WnUfmOrmRVXGkpE5O/3uo/3Mo7RhIfyy1vn/S/+2p6pQ/iATfh+UXbZP+kpVf/BZ1NsRvobzzSfrz0kG2ONdruye41FkQ7S1a1imStq+j3h8CAAAAAAAAQLztlnsF3frt1Xyqzrj77SCRXNcUan9qkuntIb2c24D33AvpysQoT5gZ7QmH2hMWLj7pTHgwyhMejfKE45X2hDOjPOGrUZ7QvoFCXcKt0Z7wS3vCDzugtoRvlYDKEj5XA+pKeOcIqCvhp/aEe1dATQnfnQEVJZy4A+pJmHkC6kk41J7QvulcXUL7DnF1Cef+gDoSjtfaE84CAVUktAdR1CW02xaWQ3ZXhztBjUrbIt6m/TBMlyptixTP3CkCvikCmm/uGH6OtkWK3S3XHDXf4NtQxAqMNPHO4zvbFim8h9Mx8zy+s22RwHug4Z7H/yEK+OT9C8zz+J62RbQPovU0EDezeTQ0/O8N8zy+/eeT+T9fzPP4C0MjcGbOO4/vb1vEifkbN53HD7QtoixC6+Gcxw+1LaIEv8c55/FXhsaRaD2NNZ2jr14HTfNesx62efxw26K5n9r1MM3ju66DpmiwZeKZxydoW5w06c+wzONTnW837FwwzOPvaAKm/vjNDdAcSAV3ZmgONG1+DK57y9BlioakD5rUdrGzmt2x7BZwI+Ezc3/boj+CCYMbir4IJVyTnH5xCyUMbyj6IpDwwL02Gv6E7VsRMngTDrlXRsWbkG6LwMyXUM+8pSdhXduiR9wJ99zLIuRM+Mm9KkrOhNJvK4niSij5hot4joSC2xYpqgn910H7qZLwi3tF1CoJpbctolH9rLRcVsJX7vXQKyeccS+nA6WEKxVtC0spoY62haWYUEnbwlJISHHpVqA8oZq2hSVPqKZtYbkm1NO2sFwSvnAvpDPni4ya2ha2fwFveoP6rW13fw+jqtoWVdp/NBEAAAAAAABAOGnPLeSdx29Sh+gZlkzz+L2pk4ubx/crziusW9RhnsfvT50rac9iZp7HD5D7XG79z1aX9lx71nn8ILnvof7PobRjIOM8/q3qsM3jx9Vp8dJzzeP3sE5Bg3n8uDot5+hZ5vF7WgcAAAAAAAAAAAAAAAAA/i9/AOyoKhG6kvueAAAAAElFTkSuQmCC;" parent="1" vertex="1">
<mxGeometry x="670" y="100" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="sKb6c6YfNXrpK8ux16pL-3" target="w-Rq3PLQNwYjt5HaCK5d-2" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="170" y="260" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="sKb6c6YfNXrpK8ux16pL-3" target="9jAj6sHaBh6Y7vf6_xip-1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="360" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="sKb6c6YfNXrpK8ux16pL-3" target="w-Rq3PLQNwYjt5HaCK5d-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="sKb6c6YfNXrpK8ux16pL-3" value="Worker Gateway" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="90" y="320" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="APDtbEWumEfnyccvbBP0-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="sKb6c6YfNXrpK8ux16pL-4" target="sKb6c6YfNXrpK8ux16pL-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="sKb6c6YfNXrpK8ux16pL-4" value="GCLS Worker Daemon" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="90" y="450" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="yRverL1VW2yy343f3klw-1" target="w-Rq3PLQNwYjt5HaCK5d-2" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="530" y="260" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="YncErUetCgZFby-mEPxv-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="yRverL1VW2yy343f3klw-1" target="9jAj6sHaBh6Y7vf6_xip-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="yRverL1VW2yy343f3klw-1" value="Consumer Gateway" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="470" y="320" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="APDtbEWumEfnyccvbBP0-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="YJTWj9HRLNrw3p02Uit6-1" target="yRverL1VW2yy343f3klw-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="YJTWj9HRLNrw3p02Uit6-1" value="GCLS Consumer CLI" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="470" y="450" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="APDtbEWumEfnyccvbBP0-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="APDtbEWumEfnyccvbBP0-6" target="sKb6c6YfNXrpK8ux16pL-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="APDtbEWumEfnyccvbBP0-6" value="Carbon Intensity Provider" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="470" y="100" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="9jAj6sHaBh6Y7vf6_xip-1" value="User Management" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="280" y="320" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="w-Rq3PLQNwYjt5HaCK5d-1" value="Worker Registry" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="90" y="100" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="w-Rq3PLQNwYjt5HaCK5d-2" value="Job" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="280" y="220" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="OQUEwtuhqqt-KE0pjZ90-1" value="Microservice" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
<mxGeometry x="860" y="72.5" width="75" height="50" as="geometry" />
</mxCell>
<mxCell id="OQUEwtuhqqt-KE0pjZ90-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="930" y="220" as="targetPoint" />
<mxPoint x="860" y="220" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="OQUEwtuhqqt-KE0pjZ90-4" value="Depends on" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="OQUEwtuhqqt-KE0pjZ90-3" vertex="1" connectable="0">
<mxGeometry x="-0.2952" y="1" relative="1" as="geometry">
<mxPoint x="5" y="-9" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="OQUEwtuhqqt-KE0pjZ90-5" value="Client Executable" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="855" y="140" width="80" height="45" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

39
doc/job.go 100644
View File

@ -0,0 +1,39 @@
package job
type Job struct {
id string
name string // descriptive name for job, helps consumers to identify job
consumerId string // defined at job creation
workerId string // defined when job assigned to worker
imageName string // Name from DockerHub, e.g. "postgres" or "chainguard/jdk"
environmentVariables map[string]string // e.g. {"POSTGRES_DB": "test_db", ...}
status JobStatus
standardOutput string
createdAt int64 // for priority -> first come first serve
// Unix timestamps in milliseconds, needed to calculate estimated CO2 equivalent:
startedAt int64
finishedAt int64
// needed for calculating the diff between executing job at worker and consumer location
ConsumerLongitude float64
ConsumerLatitude float64
Co2EquivalentEmissionConsumer float64 // emission of the consumer location (the current emission when job is scheduled)
WorkerLongitude float64
WorkerLatitude float64
Co2EquivalentEmissionWorker float64 // emission of the worker location (the current emission when job is scheduled)
// final co2 equivalent of the finished job
estimatedCo2Equivalent float64
}
type JobStatus string
const (
CREATED JobStatus = "CREATED" // job created, but not assigned to worker yet
PENDING JobStatus = "PENDING" // assigned to worker, but not running yet
RUNNING JobStatus = "RUNNING"
FINISHED JobStatus = "FINISHED"
FAILED JobStatus = "FAILED"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
doc/overview.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
doc/use-cases.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

3
go.mod 100644
View File

@ -0,0 +1,3 @@
module github.com/3000766/cmg-ws202425
go 1.23.1

View File

@ -0,0 +1,21 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/google" {
version = "6.2.0"
hashes = [
"h1:7JIgzQKRW0AT6UliiYSjYUKxDr03baZpQmt5XVkrujs=",
"zh:08a7dc0b53d2b63baab928e66086bf3e09107516af078ce011d2667456e64834",
"zh:1cf9a1373e516844b43fdcea36e73f5a68f19ad07afcf6093788eb235c710163",
"zh:2d4a7cb26c3f0d036d51db219a09013d3d779e44d584e0fc631df0f2cd5e5550",
"zh:47e1fc68e455f99f1875deaed9aa5434a852e2a70a3cb5a5e9b5a2d8c25d7b74",
"zh:78531a8624ddcd45277e1b465e773ac92001ea0e200e9dc1147ebeb24d56359e",
"zh:a76751723c034d44764df22925178f78d8b4852e3e6ac6c5d86f51666c9e666c",
"zh:a83a59a7e667cfffb0d501a501e9b3d2d4fcc83deb07a318c9690d537cbdc4b6",
"zh:b16473b7e59e01690d8234a0044c304505688f5518b205e9ed06fc63ddc82977",
"zh:b957648ad0383e17149bf3a02def81ebc6bd55ca0cffb6ec1c368a1b4f33c4fd",
"zh:e2f3f4a27b41a20bdbb7a80fbcde1a4c36bbd1c83edb9256bc1724754f8d370f",
"zh:ecfce738f85a81603aa51162d5237d6faaa2ffc0f0e52694f8b420ad761a8957",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}

View File

@ -0,0 +1,6 @@
# Infrastructure
This folder is reserved for defining the cloud infrastructure using terraform.
> **NOTE**
> Run `gcloud auth application-default login` before running terraform to gain access to the project.

View File

@ -0,0 +1,3 @@
provider "google" {
project = "cloud-infra-demo"
}

3
pkg/README.md 100644
View File

@ -0,0 +1,3 @@
# Pkg-Folder
This folder is reserved for Go packages that can be used in multiple services. It is intended for the implementation of cross cutting concerns. Do not share any domain specific code via a package. Use API calls instead.

View File

@ -0,0 +1,3 @@
module even_odd
go 1.23.2

View File

@ -0,0 +1,60 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func SortArrayByParity(Input string) (map[string][]int, error) {
//array of type int for unmarshal
var nums []int
//unmarshal the JSON String from above
err := json.Unmarshal([]byte(Input), &nums)
if err != nil {
fmt.Println("error parsing JSON:", err)
return nil, err
}
//map to store odd and even numbers seperate
numMap := map[string][]int{
"odd": {},
"even": {},
}
//sorting after odd and even
for _, num := range nums {
if num%2 == 0 {
numMap["even"] = append(numMap["even"], num)
} else {
numMap["odd"] = append(numMap["odd"], num)
}
}
//sorting the individual arrays
sort.Ints(numMap["even"])
sort.Ints(numMap["odd"])
return numMap, nil
}
func main() {
//example input
jsonInput := `[2, 5, 9, 12, 36, 43]`
sortedMap, err := SortArrayByParity(jsonInput)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Result: %v\n", sortedMap)
}
}

View File

@ -0,0 +1,98 @@
package main
import (
"encoding/json"
"errors"
"reflect"
"testing"
)
// case 1: Test the sort method with Input
func TestSortArrayByParity(t *testing.T) {
jsonInput := `[4, 50, 22, 5, 17, 32]`
expected := map[string][]int{
"even": {4, 22, 32, 50},
"odd": {5, 17},
}
result, err := SortArrayByParity(jsonInput)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Test failed. Expected %v, got %v", expected, result)
}
//case 2: empty array
jsonInput = `[]`
expected = map[string][]int{
"even": {},
"odd": {},
}
result, err = SortArrayByParity(jsonInput)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Test failed. Expected %v, got %v", expected, result)
}
//case 3: All odd numbers
jsonInput = `[1, 3, 5, 7, 9]`
expected = map[string][]int{
"even": {},
"odd": {1, 3, 5, 7, 9},
}
result, err = SortArrayByParity(jsonInput)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Test failed. Expected %v, got %v", expected, result)
}
//case 4: All even numbers
jsonInput = `[2, 4, 6, 8, 10]`
expected = map[string][]int{
"even": {2, 4, 6, 8, 10},
"odd": {},
}
result, err = SortArrayByParity(jsonInput)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Test failed. Expected %v, got %v", expected, result)
}
//case 5: invalid Format
jsonInput = `[1, 2, abc]`
_, err = SortArrayByParity(jsonInput)
if err == nil {
t.Errorf("expected error but got none")
} else {
var syntaxError *json.SyntaxError
if !errors.As(err, &syntaxError) {
t.Errorf("expected json.SyntaxError but got %T", err)
}
}
}

View File

@ -0,0 +1,3 @@
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2024219
go 1.23.1

View File

@ -0,0 +1 @@
[12, 45, 23, 67, 89, 34, 78, 90, 11, 56, 32, 77, 54, 21, 88, 99, 17, 38, 65, 48, 22, 13, 72, 83, 61, 27]

View File

@ -0,0 +1,60 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"sort"
)
func main() {
data, err := os.ReadFile("numbers.json")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
result, err := SeparateOddEven(string(data))
if err != nil {
log.Fatalf("Failed to separate odd and even numbers: %v", err)
}
PrintOddEvenMap(os.Stdout, result)
}
// SeparateOddEven sorts and separates odd and even numbers from a JSON array input
// assuming that file handling is done elsewehere and that we pass the input as a string.
func SeparateOddEven(jsonInput string) (map[string][]int, error) {
var numbers []int
err := json.Unmarshal([]byte(jsonInput), &numbers)
if err != nil {
return nil, err
}
result := make(map[string][]int)
result["odd"] = []int{}
result["even"] = []int{}
for _, num := range numbers {
if num%2 == 0 {
result["even"] = append(result["even"], num)
} else {
result["odd"] = append(result["odd"], num)
}
}
sort.Ints(result["odd"])
sort.Ints(result["even"])
return result, nil
}
// additional method for printing the result of SeparateOddEven.
// this prints to an io.Writer instead of the standard output
// to enable easy testing of the function.
func PrintOddEvenMap(w io.Writer, result map[string][]int) {
fmt.Fprintf(w, "Odd numbers: %v\n", result["odd"])
fmt.Fprintf(w, "Even numbers: %v\n", result["even"])
}

View File

@ -0,0 +1,124 @@
package main
import (
"bytes"
"reflect"
"testing"
)
// TestSeparateOddEven tests the SeparateOddEven function with various test cases.
func TestSeparateOddEven(t *testing.T) {
tests := []struct {
name string
jsonInput string
expected map[string][]int
expectedError bool
}{
{
name: "Valid input with mixed odd and even numbers",
jsonInput: `[1, 4, 5, 8, 9, 2, 11, 14, 7]`,
expected: map[string][]int{
"even": {2, 4, 8, 14},
"odd": {1, 5, 7, 9, 11},
},
expectedError: false,
},
{
name: "All even numbers",
jsonInput: `[2, 4, 6, 8, 10]`,
expected: map[string][]int{
"even": {2, 4, 6, 8, 10},
"odd": {},
},
expectedError: false,
},
{
name: "All odd numbers",
jsonInput: `[1, 3, 5, 7, 9]`,
expected: map[string][]int{
"even": {},
"odd": {1, 3, 5, 7, 9},
},
expectedError: false,
},
{
name: "Empty input",
jsonInput: `[]`,
expected: map[string][]int{"even": {}, "odd": {}},
expectedError: false,
},
{
name: "Invalid JSON input",
jsonInput: `[1, 2, "abc"]`, // Invalid JSON
expected: nil,
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := SeparateOddEven(tt.jsonInput)
if (err != nil) != tt.expectedError {
t.Errorf("SeparateOddEven() error = %v, expectedError %v", err, tt.expectedError)
return
}
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("SeparateOddEven() = %v, expected %v", result, tt.expected)
}
})
}
}
// TestPrintOddEvenMap tests the PrintOddEvenMap function by capturing the output to a buffer.
func TestPrintOddEvenMap(t *testing.T) {
tests := []struct {
description string
input map[string][]int
expectedOutput string
}{
{
description: "Mixed odd and even numbers",
input: map[string][]int{
"odd": {1, 3, 5},
"even": {2, 4, 6},
},
expectedOutput: "Odd numbers: [1 3 5]\nEven numbers: [2 4 6]\n",
},
{
description: "Only even numbers",
input: map[string][]int{
"odd": {},
"even": {2, 4, 6},
},
expectedOutput: "Odd numbers: []\nEven numbers: [2 4 6]\n",
},
{
description: "Only odd numbers",
input: map[string][]int{
"odd": {1, 3, 5},
"even": {},
},
expectedOutput: "Odd numbers: [1 3 5]\nEven numbers: []\n",
},
{
description: "Empty map",
input: map[string][]int{
"odd": {},
"even": {},
},
expectedOutput: "Odd numbers: []\nEven numbers: []\n",
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
var output bytes.Buffer
PrintOddEvenMap(&output, tc.input)
if output.String() != tc.expectedOutput {
t.Errorf("got %q, expected %q", output.String(), tc.expectedOutput)
}
})
}
}

View File

@ -0,0 +1,36 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func main() {
var jsonDatStr = "[1,2,3,4,5,6,8,9,0,10,11]"
fmt.Print(getSortedIntegerSlices(jsonDatStr))
}
func getSortedIntegerSlices(inputStr string) map[string][]int {
var inputArray []int
json.Unmarshal([]byte(inputStr), &inputArray)
var even []int
var odd []int
for _, e := range inputArray {
if e%2 == 0 {
even = append(even, e)
} else {
odd = append(odd, e)
}
sort.Ints(even)
sort.Ints(odd)
}
return map[string][]int{
"even": even,
"odd": odd,
}
}

View File

@ -0,0 +1,41 @@
package main
import (
"reflect"
"testing"
)
func TestGetSortedIntegerSlices(t *testing.T) {
testCaseMap := map[string]struct {
input string
even []int
odd []int
}{
"testA": {
input: "[4,3,2,1]",
even: []int{2, 4},
odd: []int{1, 3},
},
"testB": {
input: "[1,2,3,4,5,67]",
even: []int{2, 4},
odd: []int{1, 3, 5, 67},
},
}
for name, testCase := range testCaseMap {
t.Run(name, func(t *testing.T) {
result := getSortedIntegerSlices(testCase.input)
if !reflect.DeepEqual(result["even"], testCase.even) {
t.Errorf("For input %v: expected even %v, got %v", testCase.input, testCase.even, result["even"])
}
if !reflect.DeepEqual(result["odd"], testCase.odd) {
t.Errorf("For input %v: expected odd %v, got %v", testCase.input, testCase.odd, result["odd"])
}
})
}
}

View File

@ -0,0 +1,3 @@
module 2112107
go 1.19

View File

@ -0,0 +1,3 @@
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2121190
go 1.23.1

View File

@ -0,0 +1,15 @@
{
"integers": [
0,
1,
2,
3,
4,
10,
6,
7,
8,
9,
5
]
}

View File

@ -0,0 +1,19 @@
package main
import (
"fmt"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2121190/odd_and_even"
)
func main() {
numbers, error := odd_and_even.GetArrayFromJSON("./integers.json")
if error != nil {
fmt.Println(error)
} else {
numbersMap := odd_and_even.ConvertArrayToMapAndSort(numbers)
fmt.Println(numbersMap)
}
}

View File

@ -0,0 +1,56 @@
package odd_and_even
import (
"encoding/json"
"errors"
"io"
"os"
"sort"
)
type Integers struct {
Integers []int `json:"integers"`
}
func GetArrayFromJSON(filePath string) ([]int, error) {
jsonFile, error := os.Open(filePath)
if error != nil {
jsonFile.Close()
return nil, errors.New("File cannot be opened.")
} else {
defer jsonFile.Close() // Close after this function returns
byteValue, _ := io.ReadAll(jsonFile)
var integers Integers
json.Unmarshal(byteValue, &integers)
return integers.Integers, nil
}
}
func isEven(number int) bool {
return number%2 == 0
}
func appendAndSort(numbers []int, number int) []int {
newNumbers := append(numbers, number)
sort.Ints(newNumbers)
return newNumbers
}
func ConvertArrayToMapAndSort(numbers []int) map[string][]int {
sortedNumbersMap := make(map[string][]int)
sortedNumbersMap["odd"] = make([]int, 0)
sortedNumbersMap["even"] = make([]int, 0)
for _, number := range numbers {
if isEven(number) {
sortedNumbersMap["even"] = appendAndSort(sortedNumbersMap["even"], number)
} else {
sortedNumbersMap["odd"] = appendAndSort(sortedNumbersMap["odd"], number)
}
}
return sortedNumbersMap
}

View File

@ -0,0 +1,97 @@
package odd_and_even
import (
"reflect"
"testing"
)
func TestGetArrayFromJSONValidPath(t *testing.T) {
// GIVEN
filePath := "../integers.json"
// WHEN
numbers, error := GetArrayFromJSON(filePath)
// THEN
expectedNumbers := []int{0, 1, 2, 3, 4, 10, 6, 7, 8, 9, 5}
if !reflect.DeepEqual(numbers, expectedNumbers) {
t.Errorf("Numbers not as expected.\n Expected: %v\n Received: %v\n", expectedNumbers, numbers)
}
if error != nil {
t.Errorf("Expected no error to be thrown.\n Error: %v\n", error)
}
}
func TestGetArrayFromJSONEmptyPath(t *testing.T) {
// GIVEN
filePath := ""
// WHEN
numbers, error := GetArrayFromJSON(filePath)
// THEN
if numbers != nil {
t.Errorf("Numbers not as expected.\n Expected: %v\n Received: %v\n", nil, numbers)
}
if error == nil {
t.Errorf("Expected error to be thrown.\n")
}
}
func TestConvertArrayToMapAndSortArrayAlreadySorted(t *testing.T) {
// GIVEN
numbers := []int{0, 1, 2, 3, 4, 5}
// WHEN
numbersMap := ConvertArrayToMapAndSort(numbers)
// THEN
expectedMap := map[string][]int{
"even": []int{0, 2, 4},
"odd": []int{1, 3, 5},
}
if !reflect.DeepEqual(numbersMap, expectedMap) {
t.Errorf("NumbersMap not as expected.\n Expected: %v\n Received: %v\n", expectedMap, numbersMap)
}
}
func TestConvertArrayToMapAndSortArrayNotSorted(t *testing.T) {
// GIVEN
numbers := []int{9, 5, 7, 1, 3, 2}
// WHEN
numbersMap := ConvertArrayToMapAndSort(numbers)
// THEN
expectedMap := map[string][]int{
"even": []int{2},
"odd": []int{1, 3, 5, 7, 9},
}
if !reflect.DeepEqual(numbersMap, expectedMap) {
t.Errorf("NumbersMap not as expected.\n Expected: %v\n Received: %v\n", expectedMap, numbersMap)
}
}
func TestConvertArrayToMapAndSortArrayEmpty(t *testing.T) {
// GIVEN
numbers := []int{}
// WHEN
numbersMap := ConvertArrayToMapAndSort(numbers)
// THEN
expectedMap := map[string][]int{
"even": []int{},
"odd": []int{},
}
if !reflect.DeepEqual(numbersMap, expectedMap) {
t.Errorf("NumbersMap not as expected.\n Expected: %v\n Received: %v\n", expectedMap, numbersMap)
}
}

View File

@ -0,0 +1,3 @@
module oddEven
go 1.23.2

View File

@ -0,0 +1,7 @@
package main
import "oddEven/oddEven"
func main() {
oddEven.OddEven()
}

View File

@ -0,0 +1,3 @@
{
"numbers" : [4, 1, 3, 7, 6, 9, 0, 8, 5, 2]
}

View File

@ -0,0 +1,61 @@
package oddEven
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"slices"
)
type Numbers struct {
Numbers []int `json:"numbers"`
}
func OddEven() (map[string][]int, error) {
filepath, err := filepath.Abs("./numbers.json")
if err != nil {
fmt.Println(err)
}
jsonFile, err := os.Open(filepath)
defer jsonFile.Close()
if err != nil {
fmt.Println(err)
}
byteArray, _ := io.ReadAll(jsonFile)
var numbersjson Numbers
json.Unmarshal(byteArray, &numbersjson)
var list []int
list = numbersjson.Numbers
var odd []int
var even []int
for i := 0; i < len(list); i++ {
if list[i]%2 == 0 {
even = append(even, list[i])
} else {
odd = append(odd, list[i])
}
}
slices.Sort(odd)
slices.Sort(even)
result := map[string][]int{
"odd": odd,
"even": even,
}
fmt.Println(result)
fmt.Println(result["odd"])
return result, nil
}

View File

@ -0,0 +1,37 @@
package oddEven
import (
"os"
"reflect"
"testing"
)
func TestEvenOdd(t *testing.T) {
err := os.Chdir("..")
if err != nil {
panic(err)
}
is, err := OddEven()
if err != nil {
t.Log(err)
t.Fail()
}
should := map[string][]int{
"odd": []int{1, 3, 5, 7, 9},
"even": []int{0, 2, 4, 6, 8},
}
if !reflect.DeepEqual(is, should) {
t.Logf("Expected : %v", should["odd"])
for k := range is {
t.Log(k)
t.Log(is[k])
}
t.Logf("Was: %v", is["odd"])
t.Fail()
}
}

View File

@ -0,0 +1,3 @@
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2122245
go 1.23.1

View File

@ -0,0 +1,19 @@
package main
import (
"fmt"
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2122245/parity"
)
func main() {
numbers := "[1, 5, 3, -8, 2, -3, 4, 5, -5, 0, 42]"
seperatedNumbers, err := parity.SeperateEvenAndOddNumbers(numbers)
if err != nil {
fmt.Printf("Something went wrong: %s\n", err)
return
}
fmt.Println(seperatedNumbers)
}

View File

@ -0,0 +1,32 @@
package parity
import (
"encoding/json"
"slices"
)
func SeperateEvenAndOddNumbers(jsonString string) (map[string][]int, error) {
var numbers []int
err := json.Unmarshal([]byte(jsonString), &numbers)
if err != nil {
return nil, err
}
seperatedNumbers := map[string][]int{
"even": make([]int, 0),
"odd": make([]int, 0),
}
for _, number := range numbers {
parity := "odd"
if number%2 == 0 {
parity = "even"
}
seperatedNumbers[parity] = append(seperatedNumbers[parity], number)
}
slices.Sort(seperatedNumbers["odd"])
slices.Sort(seperatedNumbers["even"])
return seperatedNumbers, nil
}

View File

@ -0,0 +1,53 @@
package parity
import (
"fmt"
"reflect"
"testing"
)
func TestParitySeperator(t *testing.T) {
test_params := []struct {
input string
expected_output map[string][]int
}{
{"[9, 5, -5, 5, 1, 2]",
map[string][]int{
"even": {2},
"odd": {-5, 1, 5, 5, 9},
},
},
{"[4, 6, -2]",
map[string][]int{
"even": {-2, 4, 6},
"odd": {},
},
},
{"[9, -7, 1]",
map[string][]int{
"even": {},
"odd": {-7, 1, 9},
},
},
{"[]",
map[string][]int{
"even": {},
"odd": {},
},
},
}
for _, test_param := range test_params {
t.Run(fmt.Sprintf("Test with params %v", test_param), func(t *testing.T) {
output, err := SeperateEvenAndOddNumbers(test_param.input)
if err != nil {
t.Fatalf("Error: '%v'", err)
}
if !reflect.DeepEqual(output, test_param.expected_output) {
t.Fatalf("Expected '%v', but got '%v'", test_param.expected_output, output)
}
})
}
}

View File

@ -0,0 +1,3 @@
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2123801
go 1.23.1

View File

@ -0,0 +1,43 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func SortOddEven(jsonData string) (map[string][]int, error) {
var numbers []int
err := json.Unmarshal([]byte(jsonData), &numbers)
if err != nil {
return nil, err
}
result := map[string][]int{
"odd": {},
"even": {},
}
for _, num := range numbers {
if num%2 == 0 {
result["even"] = append(result["even"], num)
} else {
result["odd"] = append(result["odd"], num)
}
}
sort.Ints(result["odd"])
sort.Ints(result["even"])
return result, nil
}
func main() {
jsonData := `[5, 8, 3, 10, 1, 12, 9]`
result, err := SortOddEven(jsonData)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Ergebnis: %v\n", result)
}

View File

@ -0,0 +1,63 @@
package main
import (
"reflect"
"testing"
)
func TestSortOddEven(t *testing.T) {
tests := []struct {
input string
expected map[string][]int
hasError bool
}{
{
input: `[5, 8, 3, 10, 1, 12, 9]`,
expected: map[string][]int{
"odd": {1, 3, 5, 9},
"even": {8, 10, 12},
},
hasError: false,
},
{
input: `[2, 4, 6]`,
expected: map[string][]int{
"odd": {},
"even": {2, 4, 6},
},
hasError: false,
},
{
input: `[1, 3, 5]`,
expected: map[string][]int{
"odd": {1, 3, 5},
"even": {},
},
hasError: false,
},
{
input: `[invalid-json]`,
expected: nil,
hasError: true,
},
}
for _, test := range tests {
result, err := SortOddEven(test.input)
if test.hasError {
if err == nil {
t.Errorf("erwarteter Fehler für Eingabe %v, erhielt nil", test.input)
}
continue
}
if err != nil {
t.Errorf("erwartete keinen Fehler für Eingabe %v, erhielt %v", test.input, err)
}
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("für Eingabe %v, erwartete %v, erhielt %v", test.input, test.expected, result)
}
}
}

View File

@ -0,0 +1,57 @@
package evenOddParrity
import (
"encoding/json"
"fmt"
"log"
"os"
"sort"
)
func JSONToOddEvenMap() {
EvenOdd("integers.json")
}
func EvenOdd(filePath string) {
// Open the JSON file
file, err := os.Open(filePath)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Decode the JSON data
var data map[string][]int
decoder := json.NewDecoder(file)
err = decoder.Decode(&data)
if err != nil {
log.Fatal(err)
}
// Assume the JSON data contains a key "integers" with an array of integers
integers, ok := data["integers"]
if !ok {
log.Fatal("Key 'integers' not found in JSON")
}
// Sort the integers into even and odd
sorted := parity(integers)
fmt.Printf("Even numbers: %v\n", sorted["even"])
fmt.Printf("Odd numbers: %v\n", sorted["odd"])
}
func parity(integers []int) map[string][]int {
sorted := make(map[string][]int)
sorted["even"] = []int{}
sorted["odd"] = []int{}
for _, integer := range integers {
if integer%2 == 0 {
sorted["even"] = append(sorted["even"], integer)
} else {
sorted["odd"] = append(sorted["odd"], integer)
}
}
sort.Ints(sorted["even"])
sort.Ints(sorted["odd"])
return sorted
}

View File

@ -0,0 +1,113 @@
package evenOddParrity
import (
"bytes"
"encoding/json"
"os"
"testing"
)
func TestEvenOdd(t *testing.T) {
tests := []struct {
name string
input map[string][]int
expectedOutput string
}{
{
name: "Mixed even and odd numbers",
input: map[string][]int{
"integers": {1, 2, 3, 4, 5, 6},
},
expectedOutput: "Even numbers: [2 4 6]\nOdd numbers: [1 3 5]\n",
},
{
name: "Only even numbers",
input: map[string][]int{
"integers": {2, 4, 6, 8, 10},
},
expectedOutput: "Even numbers: [2 4 6 8 10]\nOdd numbers: []\n",
},
{
name: "Only odd numbers",
input: map[string][]int{
"integers": {1, 3, 5, 7, 9},
},
expectedOutput: "Even numbers: []\nOdd numbers: [1 3 5 7 9]\n",
},
{
name: "Empty list",
input: map[string][]int{
"integers": {},
},
expectedOutput: "Even numbers: []\nOdd numbers: []\n",
},
{
name: "Single even number",
input: map[string][]int{
"integers": {2},
},
expectedOutput: "Even numbers: [2]\nOdd numbers: []\n",
},
{
name: "Single odd number",
input: map[string][]int{
"integers": {1},
},
expectedOutput: "Even numbers: []\nOdd numbers: [1]\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock JSON content
mockJSON, err := json.Marshal(tt.input)
if err != nil {
t.Fatalf("Failed to marshal mock data: %v", err)
}
// Create a temporary file
tmpFile, err := os.CreateTemp("", "integers.json")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
// Write mock JSON content to the temporary file
if _, err := tmpFile.Write(mockJSON); err != nil {
t.Fatalf("Failed to write to temp file: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("Failed to close temp file: %v", err)
}
// Redirect stdout to a pipe
r, w, _ := os.Pipe()
old := os.Stdout
os.Stdout = w
defer func() {
os.Stdout = old
r.Close()
}()
// Create a buffer to capture the output
var buf bytes.Buffer
done := make(chan bool)
go func() {
buf.ReadFrom(r)
done <- true
}()
// Call the evenOdd function
EvenOdd(tmpFile.Name())
// Close the write end of the pipe and wait for the reading to complete
w.Close()
<-done
// Verify the output
if buf.String() != tt.expectedOutput {
t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOutput, buf.String())
}
})
}
}

View File

@ -0,0 +1,3 @@
module sortNumbers
go 1.23.1

View File

@ -0,0 +1,3 @@
{
"integers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}

View File

@ -0,0 +1,3 @@
module 2210806
go 1.23.1

View File

@ -0,0 +1,51 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"sort"
)
// ein "struct" um die geraden und ungeraden Zahlen zu speichern
type Result struct {
Odd []int `json:"odd"`
Even []int `json:"even"`
}
// oddAndEven nimmt ein JSON Array und gibt ein "Result" zuruck, mit sortierten geraden und ungeraden Zahlen.
func oddAndEven(jsonData []byte) (Result, error) {
var numbers []int
if err := json.Unmarshal(jsonData, &numbers); err != nil {
return Result{}, err
}
odd := []int{}
even := []int{}
for _, num := range numbers {
if num%2 == 0 {
even = append(even, num)
} else {
odd = append(odd, num)
}
}
sort.Ints(odd)
sort.Ints(even)
return Result{Odd: odd, Even: even}, nil
}
func main() {
jsonInput := os.Args[1]
result, err := oddAndEven([]byte(jsonInput))
if err != nil {
log.Fatalf("Error processing numbers: %v", err)
}
resultJSON, _ := json.Marshal(result)
fmt.Println(string(resultJSON))
}

View File

@ -0,0 +1,38 @@
package main
import (
"encoding/json"
"reflect"
"testing"
)
func TestProcessNumbers(t *testing.T) {
tests := []struct {
input []int
expected Result
}{
{
input: []int{1, 2, 3, 4, 5, 6},
expected: Result{Odd: []int{1, 3, 5}, Even: []int{2, 4, 6}},
},
{
input: []int{7, 8, 9, 10, 11, 12},
expected: Result{Odd: []int{7, 9, 11}, Even: []int{8, 10, 12}},
},
{
input: []int{0, -1, -2, -3},
expected: Result{Odd: []int{-3, -1}, Even: []int{-2, 0}},
},
}
for _, test := range tests {
jsonInput, _ := json.Marshal(test.input)
result, err := oddAndEven(jsonInput)
if err != nil {
t.Errorf("Error processing input %v: %v", test.input, err)
}
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("For input %v, expected %v, but got %v", test.input, test.expected, result)
}
}
}

Binary file not shown.

View File

@ -0,0 +1,51 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
// ProcessNumbers processes a JSON string of integers
func ProcessNumbers(jsonInput string) (map[string][]int, error) {
type Input struct {
Numbers []int `json:"numbers"`
}
var input Input
err := json.Unmarshal([]byte(jsonInput), &input)
if err != nil {
return nil, err
}
numberMap := map[string][]int{
"odd": {},
"even": {},
}
for _, num := range input.Numbers {
if num%2 == 0 {
numberMap["even"] = append(numberMap["even"], num)
} else {
numberMap["odd"] = append(numberMap["odd"], num)
}
}
sort.Ints(numberMap["odd"])
sort.Ints(numberMap["even"])
return numberMap, nil
}
func main() {
jsonInput := `{"numbers": [34, 21, 7, 18, 5, 12, 9, 1, 56, 4]}`
numberMap, err := ProcessNumbers(jsonInput)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Sorted Map of Odd and Even Numbers:")
fmt.Printf("Odd numbers: %v\n", numberMap["odd"])
fmt.Printf("Even numbers: %v\n", numberMap["even"])
}

View File

@ -0,0 +1,75 @@
package main
import (
"reflect"
"testing"
)
// Test ProcessNumbers function
func TestProcessNumbers(t *testing.T) {
jsonInput := `{"numbers": [34, 21, 7, 18, 5, 12, 9, 1, 56, 4]}`
expected := map[string][]int{
"odd": {1, 5, 7, 9, 21},
"even": {4, 12, 18, 34, 56},
}
result, err := ProcessNumbers(jsonInput)
if err != nil {
t.Fatalf("Error occurred: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Expected %v, but got %v", expected, result)
}
}
// Test ProcessNumbers with empty input
func TestProcessNumbersEmpty(t *testing.T) {
jsonInput := `{"numbers": []}`
expected := map[string][]int{
"odd": {},
"even": {},
}
result, err := ProcessNumbers(jsonInput)
if err != nil {
t.Fatalf("Error occurred: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Expected %v, but got %v", expected, result)
}
}
// Test ProcessNumbers with all odd numbers
func TestProcessNumbersAllOdd(t *testing.T) {
jsonInput := `{"numbers": [1, 3, 5, 7, 9]}`
expected := map[string][]int{
"odd": {1, 3, 5, 7, 9},
"even": {},
}
result, err := ProcessNumbers(jsonInput)
if err != nil {
t.Fatalf("Error occurred: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Expected %v, but got %v", expected, result)
}
}
// Test ProcessNumbers with all even numbers
func TestProcessNumbersAllEven(t *testing.T) {
jsonInput := `{"numbers": [2, 4, 6, 8, 10]}`
expected := map[string][]int{
"odd": {},
"even": {2, 4, 6, 8, 10},
}
result, err := ProcessNumbers(jsonInput)
if err != nil {
t.Fatalf("Error occurred: %v", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Expected %v, but got %v", expected, result)
}
}

View File

@ -0,0 +1,3 @@
module 2212765
go 1.23.1

View File

@ -0,0 +1,24 @@
package main
import (
"2212765/numbers"
"fmt"
"os"
)
func main() {
json, error := os.ReadFile("numbers.json")
if error != nil {
fmt.Println(error)
return
}
numbersMap, error := numbers.JSONToOddEvenMap(string(json))
if error != nil {
fmt.Println("JSONToOddEvenMap returned error: ", error)
return
}
fmt.Printf("Even: %v\nOdd: %v\n", numbersMap["even"], numbersMap["odd"])
}

View File

@ -0,0 +1 @@
[8, 1, 42, 99, 69, 88, 3, 7]

View File

@ -0,0 +1,31 @@
package numbers
import (
"encoding/json"
"sort"
)
func JSONToOddEvenMap(jsonNumbers string) (map[string][]int, error) {
var numbers []int
if error := json.Unmarshal([]byte(jsonNumbers), &numbers); error != nil {
return nil, error
}
result := map[string][]int{
"even": {},
"odd": {},
}
sort.Ints(numbers)
for _, number := range numbers {
if number%2 == 0 {
result["even"] = append(result["even"], number)
} else {
result["odd"] = append(result["odd"], number)
}
}
return result, nil
}

View File

@ -0,0 +1,55 @@
package numbers
import (
"encoding/json"
"testing"
)
type TestCase struct {
input string
expected string
}
var testCases = []TestCase{
{
input: "[1, 2, 3, 4, 5]",
expected: `{"even":[2,4],"odd":[1,3,5]}`,
},
{
input: "[5, 4, 3, 2, 1]",
expected: `{"even":[2,4],"odd":[1,3,5]}`,
},
{
input: "[1, 3, 5, 7, 9]",
expected: `{"even":[],"odd":[1,3,5,7,9]}`,
},
{
input: "[2, 4, 6, 8, 10]",
expected: `{"even":[2,4,6,8,10],"odd":[]}`,
},
{
input: "[]",
expected: `{"even":[],"odd":[]}`,
},
}
func TestJSONToOddEvenMap(t *testing.T) {
for _, testCase := range testCases {
result, error := JSONToOddEvenMap(testCase.input)
if error != nil {
t.Errorf("JSONToOddEvenMap(%q) returned error: %v",
testCase.input,
error)
}
resultJSON, _ := json.Marshal(result)
if string(resultJSON) != testCase.expected {
t.Errorf("JSONToOddEvenMap(%q) returned %s, but %s was expected",
testCase.input,
resultJSON,
testCase.expected)
}
}
}

View File

@ -0,0 +1,43 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func main() {
input := `[5, 3, 8, 1, 4, 10, 9]`
result, err := NumberProcess(input)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
}
func NumberProcess(numbers string) (map[string][]int, error) {
var intArr []int
err := json.Unmarshal([]byte(numbers), &intArr)
if err != nil {
return nil, err
}
result := map[string][]int{
"odd": {},
"even": {},
}
for _, num := range intArr {
if num%2 == 0 {
result["even"] = append(result["even"], num)
} else {
result["odd"] = append(result["odd"], num)
}
}
sort.Ints(result["odd"])
sort.Ints(result["even"])
return result, nil
}

View File

@ -0,0 +1,54 @@
package main
import (
"reflect"
"testing"
)
func TestJSONtoOddEvenMap(t *testing.T) {
tests := []struct {
input string
expected map[string][]int
hasError bool
}{
{
input: `[1, 2, 3, 4, 5]`,
expected: map[string][]int{
"odd": {1, 3, 5},
"even": {2, 4},
},
hasError: false,
},
{
input: `[-1, -2, 0, 7, 8]`,
expected: map[string][]int{
"odd": {-1, 7},
"even": {-2, 0, 8},
},
hasError: false,
},
{
input: `[1, "abc", 3]`, // Fehlerfall: falsches JSON
expected: nil,
hasError: true,
},
{
input: `[]`,
expected: map[string][]int{
"odd": {},
"even": {},
},
hasError: false,
},
}
for _, test := range tests {
result, err := NumberProcess(test.input)
if (err != nil) != test.hasError {
t.Errorf("Fehler erwartet = %v, erhalten = %v", test.hasError, err)
}
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("Erwartet %v, erhalten %v für Eingabe %v", test.expected, result, test.input)
}
}
}

View File

@ -0,0 +1,3 @@
module 3001302
go 1.22.5

View File

@ -0,0 +1,34 @@
package main
import (
"3001302/numbers"
"encoding/json"
"fmt"
"os"
)
func main() {
// Open the numbers.json file
data, err := os.ReadFile("./numbers/numbers.json")
if err != nil {
fmt.Println(err)
return
}
// Unmarshal the JSON data
var nums []int
err = json.Unmarshal(data, &nums)
if err != nil {
fmt.Println(err)
return
}
result := numbers.CategorizeNumbers(nums)
numbers.SortNumbers(result)
// Print the result
fmt.Printf("Unmarshaled: even:[%s] odd:[%s]\n",
numbers.FormatSlice(result["even"]),
numbers.FormatSlice(result["odd"]))
}

View File

@ -0,0 +1,37 @@
package numbers
import (
"fmt"
"sort"
)
// separate integers by comma
func FormatSlice(s []int) string {
var result string
for i, v := range s {
if i > 0 {
result += ", "
}
result += fmt.Sprintf("%d", v)
}
return result
}
// categorize numbers into even and odd
func CategorizeNumbers(nums []int) map[string][]int {
result := map[string][]int{"even": {}, "odd": {}}
for _, n := range nums {
if n%2 == 0 {
result["even"] = append(result["even"], n)
} else {
result["odd"] = append(result["odd"], n)
}
}
return result
}
// sort the slices in ascending order
func SortNumbers(result map[string][]int) {
sort.Ints(result["even"])
sort.Ints(result["odd"])
}

View File

@ -0,0 +1,11 @@
[
1,
5,
6,
9,
10,
13,
50,
53,
99
]

View File

@ -0,0 +1,110 @@
// numbers_test.go
package numbers
import (
"reflect"
"testing"
)
type FormatSliceTestCase struct {
input []int
expected string
}
var formatSliceTestCases = []FormatSliceTestCase{
{
input: []int{},
expected: "",
},
{
input: []int{1},
expected: "1",
},
{
input: []int{1, 2, 3},
expected: "1, 2, 3",
},
}
func TestFormatSlice(t *testing.T) {
for _, testCase := range formatSliceTestCases {
result := FormatSlice(testCase.input)
if result != testCase.expected {
t.Errorf("FormatSlice(%v) returned %q, but %q was expected",
testCase.input,
result,
testCase.expected)
}
}
}
type CategorizeNumbersTestCase struct {
input []int
expected map[string][]int
}
var categorizeNumbersTestCases = []CategorizeNumbersTestCase{
{
input: []int{},
expected: map[string][]int{"even": {}, "odd": {}},
},
{
input: []int{2, 4, 6},
expected: map[string][]int{"even": {2, 4, 6}, "odd": {}},
},
{
input: []int{1, 3, 5},
expected: map[string][]int{"even": {}, "odd": {1, 3, 5}},
},
{
input: []int{1, 2, 3, 4, 5, 6},
expected: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
},
}
func TestCategorizeNumbers(t *testing.T) {
for _, testCase := range categorizeNumbersTestCases {
result := CategorizeNumbers(testCase.input)
if !reflect.DeepEqual(result, testCase.expected) {
t.Errorf("CategorizeNumbers(%v) returned %+v, but %+v was expected",
testCase.input,
result,
testCase.expected)
}
}
}
type SortNumbersTestCase struct {
input map[string][]int
expected map[string][]int
}
var sortNumbersTestCases = []SortNumbersTestCase{
{
input: map[string][]int{"even": {}, "odd": {}},
expected: map[string][]int{"even": {}, "odd": {}},
},
{
input: map[string][]int{"even": {4, 2, 6}, "odd": {3, 1, 5}},
expected: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
},
{
input: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
expected: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
},
}
func TestSortNumbers(t *testing.T) {
for _, testCase := range sortNumbersTestCases {
SortNumbers(testCase.input)
if !reflect.DeepEqual(testCase.input, testCase.expected) {
t.Errorf("SortNumbers(%+v) returned %+v, but %+v was expected",
testCase.input,
testCase.input,
testCase.expected)
}
}
}

View File

@ -0,0 +1,3 @@
module 3001327
go 1.23.1

View File

@ -0,0 +1,42 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func Seperator(input string) (map[string][]int, error) {
var numbers []int
err := json.Unmarshal([]byte(input), &numbers)
if err != nil {
return nil, err
}
numMap := map[string][]int{
"even": {},
"odd": {},
}
for _, value := range numbers {
if value%2 == 0 {
numMap["even"] = append(numMap["even"], value)
} else {
numMap["odd"] = append(numMap["odd"], value)
}
}
sort.Ints(numMap["even"])
sort.Ints(numMap["odd"])
fmt.Println(numMap)
return numMap, nil
}
func main() {
// Define the input slice
input := `[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]`
// Call the Seperator function
Seperator(input)
}

View File

@ -0,0 +1,73 @@
package main
import (
"reflect"
"testing"
)
var example = []struct {
input string
expected map[string][]int
}{
{
input: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`,
expected: map[string][]int{
"even": {2, 4, 6, 8, 10},
"odd": {1, 3, 5, 7, 9},
},
},
{
input: `[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]`,
expected: map[string][]int{
"even": {2, 4, 6, 8, 10},
"odd": {1, 3, 5, 7, 9},
},
},
{
input: `[ -6, -5, -4, -3, -2, -1, 1, 2, 3, 4]`,
expected: map[string][]int{
"even": {-6, -4, -2, 2, 4},
"odd": {-5, -3, -1, 1, 3},
},
},
{
input: `[10, 8, 8, 7, 6, 6, 4, 3, 2, 1]`,
expected: map[string][]int{
"even": {2, 4, 6, 6, 8, 8, 10},
"odd": {1, 3, 7},
},
},
{
input: `[10, 8, 6, 4, 2]`,
expected: map[string][]int{
"even": {2, 4, 6, 8, 10},
"odd": {},
},
},
{
input: `[9, 7, 5, 3, 1]`,
expected: map[string][]int{
"even": {},
"odd": {1, 3, 5, 7, 9},
},
},
{
input: `[]`,
expected: map[string][]int{
"even": {},
"odd": {},
},
},
}
func TestSeperator(t *testing.T) {
for _, singleCase := range example {
result, err := Seperator(singleCase.input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(result, singleCase.expected) {
t.Errorf("For input %v, expected %v but got %v", singleCase.input, singleCase.expected, result)
}
}
}

View File

@ -0,0 +1,3 @@
module 3001838
go 1.23.1

View File

@ -0,0 +1,35 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func ProcessNumbers(numbers string) (map[string][]int, error) {
var intArrNrs []int
e := json.Unmarshal([]byte(numbers), &intArrNrs)
if e != nil {
return nil, e
}
result := map[string][]int{
"odd": {},
"even": {},
}
for _, num := range intArrNrs {
if num%2 == 0 {
result["even"] = append(result["even"], num)
} else {
result["odd"] = append(result["odd"], num)
}
}
sort.Ints(result["odd"])
sort.Ints(result["even"])
fmt.Println(result)
return result, nil
}

View File

@ -0,0 +1,58 @@
package main
import (
"reflect"
"testing"
)
func TestProcessNumbers(t *testing.T) {
tests := []struct {
input string
expected map[string][]int
hasError bool
}{
{
input: `[1, 4, 3, 10, 5, 2, 9, 7, 8, 6]`,
expected: map[string][]int{
"odd": {1, 3, 5, 7, 9},
"even": {2, 4, 6, 8, 10},
},
hasError: false,
},
{
input: `[]`,
expected: map[string][]int{
"odd": {},
"even": {},
},
hasError: false,
},
{
input: `[1, "two", 3]`,
expected: nil,
hasError: true,
},
}
for i, tc := range tests {
t.Logf("Running test case %d with input: %s", i+1, tc.input)
result, err := ProcessNumbers(tc.input)
if tc.hasError {
if err == nil {
t.Errorf("expected an error for input %v, but got none", tc.input)
} else {
t.Logf("Expected error received for input: %s", tc.input)
}
} else {
if err != nil {
t.Errorf("unexpected error for input %v: %v", tc.input, err)
} else {
t.Logf("Result: %v", result)
}
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("expected %v, got %v", tc.expected, result)
}
}
}
}

View File

@ -0,0 +1,3 @@
module 3002102
go 1.23.1

View File

@ -0,0 +1,34 @@
package main
import (
"encoding/json"
"fmt"
"sort"
)
func OddEvenMap(Input string) (map[string][]int, error) {
var numbers []int
err := json.Unmarshal([]byte(Input), &numbers)
if err != nil {
return nil, err
}
result := map[string][]int{
"odd": {},
"even": {},
}
for _, num := range numbers {
if num%2 == 0 {
result["even"] = append(result["even"], num)
} else {
result["odd"] = append(result["odd"], num)
}
}
sort.Ints(result["odd"])
sort.Ints(result["even"])
fmt.Println(result)
return result, nil
}

View File

@ -0,0 +1,45 @@
package main
import (
"reflect"
"testing"
)
func TestOddEvenMapJSON(t *testing.T) {
tests := []struct {
input string
expected map[string][]int
}{
{
input: `[5, 2, 9, 8, 3, 4, 7, 6, 1]`,
expected: map[string][]int{
"odd": {1, 3, 5, 7, 9},
"even": {2, 4, 6, 8},
},
},
{
input: `[10, 15, 20, 25]`,
expected: map[string][]int{
"odd": {15, 25},
"even": {10, 20},
},
},
{
input: `[]`,
expected: map[string][]int{
"odd": {},
"even": {},
},
},
}
for _, test := range tests {
result, err := OddEvenMap(test.input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("For input %v, expected %v but got %v", test.input, test.expected, result)
}
}
}

View File

@ -0,0 +1,3 @@
module numbers
go 1.23.1

Some files were not shown because too many files have changed in this diff Show More