Skip to content

Commit

Permalink
feat(api): multi lang support for actions (#728)
Browse files Browse the repository at this point in the history
* feat(api): multi lang support for actions

* undo gql changes

* lint
  • Loading branch information
pyshx authored Dec 23, 2024
1 parent 1223945 commit bfe6944
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 115 deletions.
41 changes: 36 additions & 5 deletions api/internal/app/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,31 @@ type SegregatedActions struct {
}

var (
actionsData ActionsData
once sync.Once
actionsData ActionsData
once sync.Once
supportedLangs = map[string]bool{
"en": true,
"es": true,
"fr": true,
"ja": true,
"zh": true,
}
)

func loadActionsData() error {
func loadActionsData(lang string) error {
if lang != "" && !supportedLangs[lang] {
return fmt.Errorf("unsupported language: %s", lang)
}

var err error
once.Do(func() {
// Hardcoded for now, Need to find more elegant way to deal with this @pyshx
resp, respErr := http.Get("https://raw.githubusercontent.com/reearth/reearth-flow/main/engine/schema/actions.json")
baseURL := "https://raw.githubusercontent.com/reearth/reearth-flow/main/engine/schema/"
filename := "actions.json"
if lang != "" {
filename = fmt.Sprintf("actions_%s.json", lang)
}

resp, respErr := http.Get(baseURL + filename)
if respErr != nil {
err = respErr
return
Expand Down Expand Up @@ -125,6 +141,11 @@ func listActions(c echo.Context) error {
query := c.QueryParam("q")
category := c.QueryParam("category")
actionType := c.QueryParam("type")
lang := c.QueryParam("lang")

if err := loadActionsData(lang); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}

var summaries []ActionSummary

Expand All @@ -144,6 +165,11 @@ func listActions(c echo.Context) error {

func getSegregatedActions(c echo.Context) error {
query := c.QueryParam("q")
lang := c.QueryParam("lang")

if err := loadActionsData(lang); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}

segregated := SegregatedActions{
ByCategory: make(map[string][]ActionSummary),
Expand Down Expand Up @@ -236,6 +262,11 @@ func containsCaseInsensitive(slice []string, s string) bool {

func getActionDetails(c echo.Context) error {
id := c.Param("id")
lang := c.QueryParam("lang")

if err := loadActionsData(lang); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}

for _, action := range actionsData.Actions {
if action.Name == id {
Expand Down
254 changes: 147 additions & 107 deletions api/internal/app/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,161 @@ import (
)

func TestLoadActionsData(t *testing.T) {
actionsData = ActionsData{}
once = sync.Once{}
tests := []struct {
name string
lang string
wantErr bool
}{
{"Default language", "", false},
{"English", "en", false},
{"Invalid language", "invalid", true},
}

err := loadActionsData()
assert.NoError(t, err)
assert.NotEmpty(t, actionsData.Actions)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actionsData = ActionsData{}
once = sync.Once{}
err := loadActionsData(tt.lang)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotEmpty(t, actionsData.Actions)
}
})
}
}

func TestListActions(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/actions", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
tests := []struct {
name string
query string
lang string
wantCode int
}{
{"Default language", "", "", http.StatusOK},
{"With language", "", "en", http.StatusOK},
{"Invalid language", "", "invalid", http.StatusBadRequest},
}

if assert.NoError(t, listActions(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/actions?lang="+tt.lang+tt.query, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := listActions(c)
assert.NoError(t, err)
assert.Equal(t, tt.wantCode, rec.Code)

if tt.wantCode == http.StatusOK {
var response []ActionSummary
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response)
}
})
}
}

func TestGetSegregatedActions(t *testing.T) {
originalData := actionsData
defer func() { actionsData = originalData }()

var response []ActionSummary
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response)
testActions := []Action{
{
Name: "FileWriter",
Type: ActionTypeSink,
Description: "Writes features to a file",
Categories: []string{"File"},
},
{
Name: "Router",
Type: ActionTypeProcessor,
Description: "Action for port forwarding",
Categories: []string{},
},
}
actionsData = ActionsData{Actions: testActions}

firstAction := response[0]
assert.NotEmpty(t, firstAction.Name)
assert.NotEmpty(t, firstAction.Type)
assert.NotEmpty(t, firstAction.Description)
tests := []struct {
name string
lang string
wantCode int
}{
{"Default language", "", http.StatusOK},
{"English", "en", http.StatusOK},
{"Invalid language", "invalid", http.StatusBadRequest},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/actions/segregated?lang="+tt.lang, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := getSegregatedActions(c)
assert.NoError(t, err)
assert.Equal(t, tt.wantCode, rec.Code)

if tt.wantCode == http.StatusOK {
var response SegregatedActions
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response.ByCategory)
assert.NotEmpty(t, response.ByType)
}
})
}
}

func TestGetActionDetails(t *testing.T) {
originalData := actionsData
defer func() { actionsData = originalData }()

testAction := Action{
Name: "TestAction",
Type: ActionTypeProcessor,
Description: "Test action description",
Categories: []string{"TestCategory"},
}
actionsData = ActionsData{Actions: []Action{testAction}}

tests := []struct {
name string
lang string
id string
wantCode int
}{
{"Default language", "", testAction.Name, http.StatusOK},
{"English", "en", testAction.Name, http.StatusOK},
{"Invalid language", "invalid", testAction.Name, http.StatusBadRequest},
{"Not found", "en", "NonExistent", http.StatusNotFound},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/?lang="+tt.lang, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/actions/:id")
c.SetParamNames("id")
c.SetParamValues(tt.id)

err := getActionDetails(c)
assert.NoError(t, err)
assert.Equal(t, tt.wantCode, rec.Code)

if tt.wantCode == http.StatusOK {
var response Action
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, testAction.Name, response.Name)
}
})
}
}

Expand Down Expand Up @@ -83,95 +212,6 @@ func TestListActionsWithSearch(t *testing.T) {
}
}

func TestGetSegregatedActions(t *testing.T) {
originalData := actionsData
defer func() { actionsData = originalData }()

actionsData = ActionsData{
Actions: []Action{
{
Name: "FileWriter",
Type: ActionTypeSink,
Description: "Writes features to a file",
Categories: []string{"File"},
},
{
Name: "Router",
Type: ActionTypeProcessor,
Description: "Action for last port forwarding for sub-workflows.",
Categories: []string{},
},
},
}

e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/actions/segregated", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := getSegregatedActions(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)

var response SegregatedActions
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)

assert.NotEmpty(t, response.ByCategory)
assert.NotEmpty(t, response.ByType)

assert.Contains(t, response.ByCategory, "File")
assert.Contains(t, response.ByCategory, "Uncategorized")
assert.Contains(t, response.ByType, string(ActionTypeSink))
assert.Contains(t, response.ByType, string(ActionTypeProcessor))

uncategorizedActions := response.ByCategory["Uncategorized"]
routerFound := false
for _, action := range uncategorizedActions {
if action.Name == "Router" {
routerFound = true
break
}
}
assert.True(t, routerFound, "Router should be in Uncategorized category")
}

func TestGetActionDetails(t *testing.T) {
originalData := actionsData
defer func() { actionsData = originalData }()

testAction := Action{
Name: "TestAction",
Type: ActionTypeProcessor,
Description: "Test action description",
Categories: []string{"TestCategory"},
}

actionsData = ActionsData{
Actions: []Action{testAction},
}

e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/actions/:id")
c.SetParamNames("id")
c.SetParamValues(testAction.Name)

err := getActionDetails(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)

var response Action
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, testAction.Name, response.Name)
assert.Equal(t, testAction.Description, response.Description)
assert.Equal(t, testAction.Type, response.Type)
assert.Equal(t, testAction.Categories, response.Categories)
}

func TestGetActionDetailsNotFound(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
Expand Down
14 changes: 11 additions & 3 deletions api/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,8 @@ func initEcho(ctx context.Context, cfg *ServerConfig) *echo.Echo {
apiPrivate.POST("/signup/verify/:code", SignupVerify())
apiPrivate.POST("/password-reset", PasswordReset())
}

if err := loadActionsData(); err != nil {
log.Errorf("Failed to load actions data: %v", err)
if err := initActionsData(ctx); err != nil {
log.Errorf("Failed to initialize actions data: %v", err)
}
SetupActionRoutes(e)

Expand All @@ -115,6 +114,15 @@ func initEcho(ctx context.Context, cfg *ServerConfig) *echo.Echo {
return e
}

func initActionsData(ctx context.Context) error {
for lang := range supportedLangs {
if err := loadActionsData(lang); err != nil {
log.Errorf("Failed to load actions data for language %s: %v", lang, err)
}
}
return nil
}

func errorHandler(next func(error, echo.Context)) func(error, echo.Context) {
return func(err error, c echo.Context) {
if c.Response().Committed {
Expand Down

0 comments on commit bfe6944

Please sign in to comment.