Yet another useless piece of software (at least it's not a TODO API :kekw:) to showcase my Go dev skills. LineCodeAPI provides an interface both Web (REST) and gRPC for generating and storing (in a database) Line Codes, a technique used to convert digital data in a pattern of analog signals. For instance, a Manchester line code represents logical zeros as a Rising Edge (-A to +A), and logical ones as a Falling Edge (+A to -A).
- Get Started (EZ Docker Run)
- App Architecture
- Manchester Model
- Domain Logic CODEC
- Data Persistence with GORM
- Web REST API with net/http (Go 1.22+)
- gRPC Server and Protobuf
- Containerizing with Docker
This app can be easily run with Docker. An "example-docker-compose.yaml" file is provided inside the repo, copy the file as "docker-compose.yaml" and start it with docker compose.
cp example-docker-compose.yaml docker-compose.yamldocker compose upOnce the app is running, the Web Server will be listening at:
localhost:8080
With endpoints:
GET {{url}}/manchester
POST {{url}}/manchester/encoder
bodyExample = {
"decoded": "0AE3",
"decodedPulseWidth": 400,
"unit": "us"
}
POST {{url}}/manchester/decoder
bodyExample = {
"encoded": "-A+A+A-A+A-A+A-A+A-A+A-A+A-A-A+A",
"encodedPulseWidth": 400,
"unit": "us"
}
To test de gRPC API (listening on localhost:9000), I used "grpc-cli" for my linux env (Arch btw):
grpc_cli ls localhost:9000
grpc_cli call localhost:9000 ManchesterEncode "decoded: '00E0FF', decodedPulseWidth: 200, unit: 'us'"
grpc_cli call localhost:9000 ManchesterDecode "encoded: '-A+A+A-A+A-A+A-A+A-A+A-A+A-A-A+A', decodedPulseWidth: 200, unit: 'us'"
But any gRPC client should work.
This Software uses an Hexagonal Architecture with 3 concentric layers:
- A core with domain logic and models (without dependencies)
- An Application Layer with the "ports" and the use cases.
- An Adapters Layer with all the "adapters" both driven an drivers!
Note: The domain logic is not connected through ports, it is injected (Dependency Injection) on the main func though.
Ports are implemented using Go interfaces, including de API port (not a Service but API on the wider definition) which is a "reversed port" required to connect the driver adapters to their respective use cases. Most ports are defined in the "ports.go" file but the gRPC port is auto-generated by Protobuf as a Service.
Adapters are Go structs that implement code with external dependencies, each one will be explained on further sections. All adapters should be instantiated in the app entrypoint (main file) and mounted with Dependency Injection.
There are a lot of line codes, this app (at the moment) provides a Manchester CODEC with a data model represented by the following JSON example:
{
"decoded": "AE01",
"encoded": "+A-A-A+A+A-A-A+A+A-A+A-A+A-A-A+A-A+A-A+A-A+A-A+A-A+A-A+A-A+A+A-A",
"decodedPulseWidth": 200,
"encodedPulseWidth": 100,
"unit": "ns"
}Notice that:
- Decoded is a string of a hex representation of binary data. It MUST be valid bytes!
- Encoded is the Manchester encoded data as a sequence of signal amplitudes (+A or -A) represented with a string.
- Both pulse widths represent the time duration of a signal pulse or digital data on a "clock" context.
- Unit is the time unit in seconds of the pulse width.
You may be wondering: Why would someone build such a useless API? (line codes are quite important but at Hardware lvl or Telecom)... Well idk, I was kinda out of ideas but hear me out, Go is LOVELY you can easily do low level stuff lik this CODEC with some powerful tools like hash maps!
The idea behind Manchester encoding is pretty easy:
var in bool
in = getBit()
if in {
return "+A-A"
} else {
return "-A+A"
}This works great on low level with perfect control of the CPU clock (or MicroController), as a Service maybe it is not the best idea to check every bit every time for every request!
Therefore, the app generates a byte (8 bit) grouping dictionary and stores it on RAM as a Go map. This is an efficient way to get the encoded pattern of a byte without looping, reducing time complexity from O(n) to O(1), and consequently (with a chain of bytes) from O(n²) to O(n).
The dictionary constructing algorithm basically takes a byte and loops through this byte, it also uses a byte mask with an AND operator to check if a bit is set or cleared and appends the Manchester signal to the output.
Decoding uses the same aproach but reverted!
This Adapter uses GORM to communicate with a Postgres DataBase, on creation, a model migration is performed (Code First). Connection parameters are specified as OS Environmental Variables!
The driven adapter implements the database port interface and follows GORM's approach of passing model's pointers as arguments. The port is generic over the data model (DbPort[T]), so its methods are reusable between data models.
Persistence should improve the app efficiency by searching the line code before creating it, avoiding unnecessary usage of the core logic.
All configuration is provided through OS environment variables:
| Variable | Description | Default |
|---|---|---|
DB_SERVER |
Postgres host | (none) |
DB_USER |
Postgres user | (none) |
DB_PASSWORD |
Postgres password | (none) |
DB_PORT |
Postgres port | (none) |
DB_NAME |
Postgres database name | (none) |
DB_SSL_MODE |
Postgres SSL mode (e.g. disable) |
(none) |
DB_TIME_ZONE |
Postgres time zone (e.g. UTC) |
(none) |
WEB_PORT |
Port for the Web (REST) server | 8080 |
GRPC_PORT |
Port for the gRPC server | 9000 |
The DB variables are required; WEB_PORT and GRPC_PORT are optional and fall back to their defaults when unset.
This is a driver Adapter, it runs a Web Server using the standard library's net/http package and the application api port. The requests are handled with the native ServeMux (leveraging Go 1.22+ routing enhancements). The listening port is configurable through the WEB_PORT env var (default 8080).
Some notes:
- The adapter can be run with a go routine using the RunAsync Method, the main func should pass a context and a wait group to sync the execution!
- The server shuts down gracefully when the context is cancelled (on
SIGINT/SIGTERM), draining in-flight requests with a 10s timeout. - A simple Postman collection is provided inside the repo with some examples of requests.
- Middleware can be added using standard
http.Handlerwrappers.
This is also a driver Adapter that runs a server, it uses Protobuf for go in order to generate the service code. The "linecode_svc.proto" file contains both service and message types. The listening port is configurable through the GRPC_PORT env var (default 9000).
Some notes:
- The service interface is auto-generated and passed to the adapter through the "UnimplementedLineCoderServer" (also auto-generated).
- Reflection is activated for testing with grpc-cli!
- The adapter can be run async (needed if you use several servers) with the RunAsync method, just like the REST API.
- It also shuts down gracefully (via
GracefulStop) when the context is cancelled, finishing in-flight RPCs first.
A Dockerfile is included, inside:
- A Go with Alpine Linux image is pulled
- Source files are copied and dependencies are re-downloaded
- The app is built and the binary is properly relocated
- The app is executed through a simple entrypoint script that delays the init some seconds.
An example of "docker-compose.yaml" is also included, inside:
- A postgres DB image is pulled and run.
- The app container is built with OS env vars and link dependencies.
- A PGAdmin image is pulled and run for DB testing (this can be deleted).