Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
with:
go-version: "1.23"
- name: Run coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*.*.*
jobs:
package:
runs-on: ubuntu-latest
env:
dockerfile: Dockerfile
image_name: iomz/golemu
platforms: linux/amd64,linux/arm64
registry: ghcr.io
steps:
- uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
${{ env.registry }}/${{ env.image_name }}
# generate Docker tags based on the following events/attributes
tags: |
type=schedule
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: "${{ env.platforms }}"
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
*.swp
sim/*
vendor/*

coverage.out
golemu
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright © 2016 Iori MIZUTANI <iori.mizutani@gmail.com>
Copyright © 2025 Iori MIZUTANI <iori.mizutani@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
Expand Down
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,35 @@ Mizutani, I., & Mitsugi, J. (2016). A Multicode and Portable RFID Tag Events Emu

# Installation

Install [dep](https://github.com/golang/dep) in your system first.
## From Source

```bash
# Install the latest version
go install github.com/iomz/golemu/cmd/golemu@latest

# Or install from a local clone
git clone https://github.com/iomz/golemu.git
cd golemu
go install ./cmd/golemu

# Verify installation
golemu --help
```
$ go get github.com/iomz/golemu
$ cd $GOPATH/src/github.com/iomz/golemu
$ dep ensure && go install .

**Note:** Make sure `$GOPATH/bin` or `$HOME/go/bin` is in your `PATH` environment variable to use the `golemu` command directly.

## Build Locally

```bash
# Clone the repository
git clone https://github.com/iomz/golemu.git
cd golemu

# Build the binary
go build -o golemu ./cmd/golemu

# Run directly
./golemu --help
```

# Synopsis
Expand Down Expand Up @@ -98,4 +121,4 @@ See the LICENSE file.

## Author

Iori Mizutani (iomz)
Iori Mizutani (@iomz)
249 changes: 249 additions & 0 deletions api/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//
// Use of this source code is governed by The MIT License
// that can be found in the LICENSE file.

package api

import (
"errors"
"fmt"
"net/http"

"github.com/fatih/structs"
"github.com/gin-gonic/gin"
"github.com/iomz/go-llrp"
"github.com/iomz/golemu/tag"
log "github.com/sirupsen/logrus"
)

// validationError represents an error that occurred during tag validation.
type validationError struct {
message string
details []string
}

func (e *validationError) Error() string {
return e.message
}

// notFoundError represents an error when one or more tags are not found in storage.
type notFoundError struct {
message string
}

func (e *notFoundError) Error() string {
return e.message
}

// duplicateTagError represents an error when one or more tags already exist.
type duplicateTagError struct {
message string
}

func (e *duplicateTagError) Error() string {
return e.message
}

// Handler processes HTTP API requests for tag management operations.
// It provides REST endpoints for adding, deleting, and retrieving tags.
type Handler struct {
tagManagerChan chan tag.Manager
}

// NewHandler creates a new API handler with the specified tag management channel.
//
// Parameters:
// - tagManagerChan: Channel for sending tag management commands
func NewHandler(tagManagerChan chan tag.Manager) *Handler {
return &Handler{
tagManagerChan: tagManagerChan,
}
}

// PostTag handles HTTP POST requests to add new tags.
// It expects a JSON array of TagRecord objects in the request body.
// Returns 201 Created on success, 400 Bad Request for invalid JSON or validation errors,
// or 409 Conflict if one or more tags already exist.
func (h *Handler) PostTag(c *gin.Context) {
var json []llrp.TagRecord
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()})
return
}

count, err := h.reqAddTag(json)
if err != nil {
var validationErr *validationError
var duplicateErr *duplicateTagError
if errors.As(err, &validationErr) {
c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.message, "details": validationErr.details})
} else if errors.As(err, &duplicateErr) {
c.JSON(http.StatusConflict, gin.H{"error": duplicateErr.message})
} else {
log.Errorf("unexpected error in PostTag: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
}
return
}

c.JSON(http.StatusCreated, gin.H{"message": "Tags added successfully", "count": count})
}

// DeleteTag handles HTTP DELETE requests to remove tags.
// It expects a JSON array of TagRecord objects in the request body.
// Returns 200 OK on success, 400 Bad Request for invalid JSON or validation errors,
// 404 Not Found if one or more tags do not exist, or 500 Internal Server Error
// for unexpected errors.
func (h *Handler) DeleteTag(c *gin.Context) {
var json []llrp.TagRecord
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()})
return
}

err := h.reqDeleteTag(json)
if err != nil {
var validationErr *validationError
var notFoundErr *notFoundError
if errors.As(err, &validationErr) {
c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.message, "details": validationErr.details})
} else if errors.As(err, &notFoundErr) {
c.JSON(http.StatusNotFound, gin.H{"error": notFoundErr.message})
} else {
log.Errorf("unexpected error in DeleteTag: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
}
return
}

c.JSON(http.StatusOK, gin.H{"message": "Tags deleted successfully"})
}

// GetTags handles HTTP GET requests to retrieve all tags.
// Returns 200 OK with a JSON array of all currently stored tags.
func (h *Handler) GetTags(c *gin.Context) {
tagList := h.reqRetrieveTag()
c.JSON(http.StatusOK, tagList)
}

func (h *Handler) reqAddTag(req []llrp.TagRecord) (int, error) {
validTags := []*llrp.Tag{}
validationErrors := []string{}
for i, t := range req {
tagObj, err := llrp.NewTag(&llrp.TagRecord{
PCBits: t.PCBits,
EPC: t.EPC,
})
if err != nil {
log.Errorf("error creating tag: %v", err)
validationErrors = append(validationErrors, fmt.Sprintf("tag[%d]: %v", i, err))
continue
}

validTags = append(validTags, tagObj)
}

// If there were validation errors, return validationError
if len(validationErrors) > 0 {
return 0, &validationError{
message: "One or more tags failed validation",
details: validationErrors,
}
}

// Send add commands and wait for responses to check for duplicates
totalRequested := len(validTags)
totalAdded := 0

for _, tagObj := range validTags {
add := tag.Manager{
Action: tag.AddTags,
Tags: []*llrp.Tag{tagObj},
}
h.tagManagerChan <- add
// Wait for response to check if tag was actually added
response := <-h.tagManagerChan
if len(response.Tags) > 0 {
totalAdded += len(response.Tags)
}
}

// Check if all requested tags were actually added (detect duplicates)
if totalAdded < totalRequested {
return totalAdded, &duplicateTagError{
message: fmt.Sprintf("One or more tags already exist (%d requested, %d added)", totalRequested, totalAdded),
}
}

log.Debugf("add %v", req)
return totalAdded, nil
}

func (h *Handler) reqDeleteTag(req []llrp.TagRecord) error {
// First, validate all tags and collect validation errors
validTags := []*llrp.Tag{}
validationErrors := []string{}
for i, t := range req {
tagObj, err := llrp.NewTag(&llrp.TagRecord{
PCBits: t.PCBits,
EPC: t.EPC,
})
if err != nil {
log.Errorf("error creating tag: %v", err)
validationErrors = append(validationErrors, fmt.Sprintf("tag[%d]: %v", i, err))
continue
}
validTags = append(validTags, tagObj)
}

// If there were validation errors, return validationError
if len(validationErrors) > 0 {
return &validationError{
message: "One or more tags failed validation",
details: validationErrors,
}
}

// Send delete commands and wait for responses
totalRequested := len(validTags)
totalDeleted := 0

for _, tagObj := range validTags {
deleteCmd := tag.Manager{
Action: tag.DeleteTags,
Tags: []*llrp.Tag{tagObj},
}
h.tagManagerChan <- deleteCmd
// Wait for response to check if tag was actually deleted
response := <-h.tagManagerChan
if len(response.Tags) > 0 {
totalDeleted += len(response.Tags)
}
}

// Check if all requested tags were actually deleted
if totalDeleted < totalRequested {
return &notFoundError{
message: fmt.Sprintf("One or more tags not found (%d requested, %d deleted)", totalRequested, totalDeleted),
}
}

log.Debugf("delete %v", req)
return nil
}

func (h *Handler) reqRetrieveTag() []map[string]interface{} {
retrieve := tag.Manager{
Action: tag.RetrieveTags,
Tags: []*llrp.Tag{},
}
h.tagManagerChan <- retrieve
retrieve = <-h.tagManagerChan
var tagList []map[string]interface{}
for _, tagObj := range retrieve.Tags {
t := structs.Map(llrp.NewTagRecord(*tagObj))
tagList = append(tagList, t)
}
log.Debugf("retrieve: %v", tagList)
return tagList
}
Loading