Skip to content

Commit

Permalink
merge initial code for aepc
Browse files Browse the repository at this point in the history
poc for aepc

- with resource definition support
- parameter support
- proto writer
- proto / json / yaml reader
  • Loading branch information
toumorokoshi authored Oct 4, 2023
2 parents 53afb77 + c388afd commit f237fd2
Show file tree
Hide file tree
Showing 19 changed files with 1,834 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
9 changes: 9 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Development

## Building aepc

The standard GoLang toolchain is used, with the addition of protobuf for
compiling the resource definition.

1. `protoc ./schema/resourcedefinition.proto --go_opt paths=source_relative --go_out=.`
2. `go build main.go`
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# aepc
# aepc

Generates AEP-compliant RPCs from proto messages. See the main README.md for usage.

## Purpose

aepc is designed to primarily work off of a resource model: rather than having individual RPCs / methods on a resource, the user declares *resources* that live under a *service*. The common operations against a resource (Create, Read, Update, List, and Delete) are generatable based on the desired control plane standard, such as the AEP standard or custom resource definitions for the Kubernetes Resource Model.

## Design

aepc works off of an internal "hub" representation of a resource, while each of the consumers and producers is a "spoke", using the resource information for generation of service, clients, or documentation:

```mermaid
flowchart LR
hub("unified service and resource hub")
protoResources("proto messages")
proto("protobuf")
crd("Custom Resource Definitions (K8S)")
http("HTTP REST APIs")
protoResources --> hub
hub --> proto
hub --> http
hub --> crd
```

## User Guide

```
go run main.go -i ./examples/bookstore.yaml -o ./examples/bookstore.yaml.output.proto
```
140 changes: 140 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2023 Yusuke Fredrick Tsutsumi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd

import (
"fmt"
"io"
"log"
"os"
"path/filepath"

"github.com/ghodss/yaml"

"github.com/aep-dev/aepc/loader"
"github.com/aep-dev/aepc/parser"
"github.com/aep-dev/aepc/schema"
"github.com/aep-dev/aepc/validator"
"github.com/aep-dev/aepc/writer/proto"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
)

func NewCommand() *cobra.Command {
var inputFile string
var outputFile string

c := &cobra.Command{
Use: "aepc",
Short: "aepc compiles resource representations to full proto rpcs",
Long: "aepc compiles resource representations to full proto rpcs",
Run: func(cmd *cobra.Command, args []string) {
// TODO: error handling
s := &schema.Service{}
input, err := readFile(inputFile)
fmt.Printf("input: %s\n", string(input))
if err != nil {
log.Fatalf("unable to read file: %v", err)
}
ext := filepath.Ext(inputFile)
err = unmarshal(ext, input, s)
if err != nil {
log.Fatal(err)
}
errors := validator.ValidateService(s)
if len(errors) > 0 {
log.Fatalf("error validating service: %v", errors)
}
ps, err := parser.NewParsedService(s)
if err != nil {
log.Fatal(err)
}
proto, _ := proto.WriteServiceToProto(ps)

err = writeFile(outputFile, proto)
if err != nil {
log.Fatal(err)
}
fmt.Printf("output file: %s\n", outputFile)
fmt.Printf("output proto: %s\n", proto)
},
}
c.Flags().StringVarP(&inputFile, "input", "i", "", "input files with resource")
c.Flags().StringVarP(&outputFile, "output", "o", "", "output file to use")
return c
}

func unmarshal(ext string, b []byte, s *schema.Service) error {
switch ext {
case ".proto":
if err := loader.ReadServiceFromProto(b, s); err != nil {
return fmt.Errorf("unable to decode proto %q: %w", string(b), err)
}
case ".yaml":
asJson, err := yaml.YAMLToJSON(b)
if err != nil {
return fmt.Errorf("unable to decode yaml to JSON %q: %w", string(b), err)
}
if err := protojson.Unmarshal(asJson, s); err != nil {
log.Fatal(fmt.Errorf("unable to decode proto %q: %w", string(b), err))
}
case ".json":
if err := protojson.Unmarshal(b, s); err != nil {
return fmt.Errorf("unable to decode json %q: %w", string(b), err)
}
default:
return fmt.Errorf("extension %v is unsupported", ext)
}
return nil
}

func readFile(fileName string) ([]byte, error) {
var value []byte
f, err := os.OpenFile(fileName, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
bytesRead := 1
for bytesRead > 0 {
readBytes := make([]byte, 10000)
bytesRead, err = f.Read(readBytes)
if bytesRead > 0 {
value = append(value, readBytes[:bytesRead]...)
}
if err != io.EOF && err != nil {
return nil, err
}
}
err = f.Close()
if err != nil {
return nil, err
}
return value, nil
}

func writeFile(fileName string, value []byte) error {
f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
_, err = f.Write(value)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
return nil
}
20 changes: 20 additions & 0 deletions examples/bookstore.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// NOTE: since we are missing some proto annotations that
// must be added, the proto representation is outdated. See bookstore.yaml
syntax = "proto3";
package tutorial;
option go_package = "proto";

// bookstore.examples.com
service Bookstore {
}

message Book {

// represents the isbn.
string isbn = 3;
message Properties {}
message Status {}

Properties properties = 1;
Status status = 2;
}
29 changes: 29 additions & 0 deletions examples/bookstore.proto.output.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto3";

import "google/api/annotations.proto";

option go_package = "/newbookstore";

message book {
string path = 1;
}

message CreatebookRequest {
string id = 1;

book resource = 2;
}

message ReadbookRequest {
string path = 1;
}

service NewBookStore {
rpc Createbook ( CreatebookRequest ) returns ( book ) {
option (google.api.http) = { post: "/book" };
}

rpc Readbook ( ReadbookRequest ) returns ( book ) {
option (google.api.http) = { get: "/{path=book/*}" };
}
}
28 changes: 28 additions & 0 deletions examples/bookstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: "bookstore.example.com"
resources:
- kind: "Book"
properties:
isbn:
type: STRING
number: 1
parents:
- "bookstore.example.com/Publisher"
methods:
create: {}
read: {}
update: {}
delete: {}
list: {}
- kind: "Publisher"
methods:
read: {}
list: {}
- kind: "Author"
properties:
name:
type: STRING
number: 1
parents:
- "Publisher"
methods:
read: {}
101 changes: 101 additions & 0 deletions examples/bookstore.yaml.output.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
syntax = "proto3";

import "google/api/annotations.proto";

import "google/protobuf/empty.proto";

option go_package = "/bookstore";

message Author {
string path = 10000;

string name = 1;
}

message ReadAuthorRequest {
string path = 1;
}

message Book {
string path = 10000;

string isbn = 1;
}

message CreateBookRequest {
string id = 1;

Book resource = 2;
}

message ReadBookRequest {
string path = 1;
}

message UpdateBookRequest {
string path = 1;

UpdateBookRequest resource = 2;
}

message DeleteBookRequest {
string path = 1;
}

message ListBookRequest {
string path = 1;
}

message ListBookResponse {
repeated Book resources = 1;
}

message Publisher {
string path = 10000;
}

message ReadPublisherRequest {
string path = 1;
}

message ListPublisherRequest {
string path = 1;
}

message ListPublisherResponse {
repeated Publisher resources = 1;
}

service Bookstore {
rpc ReadAuthor ( ReadAuthorRequest ) returns ( Author ) {
option (google.api.http) = { get: "/{path=publisher/*/author/*}" };
}

rpc CreateBook ( CreateBookRequest ) returns ( Book ) {
option (google.api.http) = { post: "/{parent=publisher/*}/book" };
}

rpc ReadBook ( ReadBookRequest ) returns ( Book ) {
option (google.api.http) = { get: "/{path=publisher/*/book/*}" };
}

rpc UpdateBook ( UpdateBookRequest ) returns ( Book ) {
option (google.api.http) = { get: "/{resource.name=publisher/*/book/*}" };
}

rpc DeleteBook ( DeleteBookRequest ) returns ( google.protobuf.Empty ) {
option (google.api.http) = { delete: "/{path=publisher/*/book/*}" };
}

rpc ListBook ( ListBookRequest ) returns ( ListBookResponse ) {
option (google.api.http) = { get: "/{parent=publisher/*}/book" };
}

rpc ReadPublisher ( ReadPublisherRequest ) returns ( Publisher ) {
option (google.api.http) = { get: "/{path=publisher/*}" };
}

rpc ListPublisher ( ListPublisherRequest ) returns ( ListPublisherResponse ) {
option (google.api.http) = { get: "/publisher" };
}
}
Loading

0 comments on commit f237fd2

Please sign in to comment.