Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev/jgough/10216 support eventsv1 #41

Merged
merged 17 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 15 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
5 changes: 3 additions & 2 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: '1.23'
cache: false

- name: Install Go quality tools
run: |
go install golang.org/x/tools/cmd/[email protected]
go install github.com/axw/gocov/[email protected]
go install github.com/jstemmer/go-junit-report/[email protected]
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.60.3

- name: Install Task
uses: arduino/setup-task@v1
with:
Expand Down
2 changes: 1 addition & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func NewApp(version string, ikwid bool) *cli.App {
Usage: "common read only operations on datatrails merklelog verifiable data",
Flags: []cli.Flag{
&cli.StringFlag{Name: "loglevel", Value: "NOOP"},
&cli.Int64Flag{Name: "height", Value: 14, Usage: "override the massif height"},
&cli.Int64Flag{Name: "height", Value: int64(defaultMassifHeight), Usage: "override the massif height"},
&cli.StringFlag{
Name: "data-url", Aliases: []string{"u"},
Usage: "url to download merkle log data from. mutually exclusive with data-local; if neither option is supplied, DataTrails' live log data will be used",
Expand Down
199 changes: 199 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package app

import (
"bytes"
"encoding/json"
"errors"
"io"
"strings"

"github.com/datatrails/go-datatrails-logverification/logverification/app"
)

/**
* Merklelog related APP contents
*
* An APP in the context of merklelog is an interface that commits log entries.
*
* Apps include:
* - assetsv2
* - eventsv1
*/

const (
AssetsV2AppDomain = byte(0)
EventsV1AppDomain = byte(1)
)

var (
ErrNoJsonGiven = errors.New("no json given")
)

// AppDataToVerifiableLogEntries converts the app data to verifiable log entries
func AppDataToVerifiableLogEntries(appData []byte, logTenant string) ([]app.AppEntry, error) {
honourfish marked this conversation as resolved.
Show resolved Hide resolved

// first attempt to convert the appdata to a list of events
eventList, err := eventListFromData(appData)
if err != nil {
return nil, err
}

// now we have an event list we can decipher if the app is
henry739 marked this conversation as resolved.
Show resolved Hide resolved
// assetsv2 or eventsv1
appDomain := appDomain(appData)

verifiableLogEntries := []app.AppEntry{}

switch appDomain {
case AssetsV2AppDomain:
// assetsv2
verfiableAssetsV2Events, err := NewAssetsV2AppEntries(eventList)
if err != nil {
return nil, err
}

verifiableLogEntries = append(verifiableLogEntries, verfiableAssetsV2Events...)

case EventsV1AppDomain:
verfiableEventsV1Events, err := NewEventsV1AppEntries(eventList, logTenant)
if err != nil {
return nil, err
}

verifiableLogEntries = append(verifiableLogEntries, verfiableEventsV1Events...)

default:
return nil, errors.New("unknown app domain for given app data")
}

return verifiableLogEntries, nil
}

// appDomain returns the app domain of the given app data
func appDomain(appData []byte) byte {

// first attempt to convert the appdata to a list of events
eventList, err := eventListFromData(appData)
if err != nil {
// if we can't return default of assetsv2
return AssetsV2AppDomain
}

// decode into events
events := struct {
henry739 marked this conversation as resolved.
Show resolved Hide resolved
Events []json.RawMessage `json:"events,omitempty"`
NextPageToken json.RawMessage `json:"next_page_token,omitempty"`
}{}

decoder := json.NewDecoder(bytes.NewReader(eventList))
decoder.DisallowUnknownFields()
for {
err = decoder.Decode(&events)

if errors.Is(err, io.EOF) {
break
}

if err != nil {
// return default of assetsv2
return AssetsV2AppDomain
}
}

// decode the first event and find the identity
event := struct {
Identity string `json:"identity,omitempty"`
}{}

decoder = json.NewDecoder(bytes.NewReader(events.Events[0]))
decoder.DisallowUnknownFields()

for {
err = decoder.Decode(&event)
henry739 marked this conversation as resolved.
Show resolved Hide resolved

if errors.Is(err, io.EOF) {
break
}

if err != nil {
// if we can't return default of assetsv2
return AssetsV2AppDomain
}
}

// find if the event identity is assetsv2 or eventsv1 identity
if strings.HasPrefix(event.Identity, "assets/") {
return AssetsV2AppDomain
} else {
return EventsV1AppDomain
}

}

// eventListFromData normalises a json encoded event or *list* of events, by
henry739 marked this conversation as resolved.
Show resolved Hide resolved
// always returning a list of json encoded events.
//
// This converts events from the following apps:
// - assetsv2
// - eventsv1
//
// NOTE: there is no json validation done on the event or list of events given
// any valid json will be accepted, use validation logic after this function.
func eventListFromData(data []byte) ([]byte, error) {
var err error

doc := struct {
Events []json.RawMessage `json:"events,omitempty"`
NextPageToken json.RawMessage `json:"next_page_token,omitempty"`
}{}

// check for empty json
// NOTE: also len(nil) == 0, so does the nil check also
if len(data) == 0 {
return nil, ErrNoJsonGiven
}

decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()

honourfish marked this conversation as resolved.
Show resolved Hide resolved
for {

err = decoder.Decode(&doc)

// if we can decode the events json
// we know its in the form of a list events json response from
// the list events api, so just return data
if errors.Is(err, io.EOF) {
return data, nil
}

if err != nil {
break
}

}

// if we get here we know that the given data doesn't represent
// a list events json response
// so we can assume its a single event response from the events api.

var event json.RawMessage
err = json.Unmarshal(data, &event)
if err != nil {
return nil, err
}

// purposefully omit the next page token for response
listEvents := struct {
Events []json.RawMessage `json:"events,omitempty"`
}{}

listEvents.Events = []json.RawMessage{event}

events, err := json.Marshal(&listEvents)
if err != nil {
return nil, err
}

return events, nil
}
77 changes: 77 additions & 0 deletions app/app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package app

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestEventListFromData(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
expected []byte
wantErr bool
}{
{
name: "nil",
args: args{
data: nil,
},
expected: nil,
wantErr: true,
},
{
name: "empty",
args: args{
data: []byte{},
},
expected: nil,
wantErr: true,
},
// We do need this, since we expect input from other processes via pipes (i.e. an events query)
{
name: "empty list",
args: args{
data: []byte(`{"events":[]}`),
},
expected: []byte(`{"events":[]}`),
wantErr: false,
},
{
name: "single event",
args: args{
data: []byte(`{"identity":"assets/1/events/2"}`),
},
expected: []byte(`{"events":[{"identity":"assets/1/events/2"}]}`),
wantErr: false,
},
{
name: "single list",
args: args{
data: []byte(`{"events":[{"identity":"assets/1/events/2"}]}`),
},
expected: []byte(`{"events":[{"identity":"assets/1/events/2"}]}`),
wantErr: false,
},
{
name: "multiple list",
args: args{
data: []byte(`{"events":[{"identity":"assets/1/events/2"},{"identity":"assets/1/events/3"}]}`),
},
expected: []byte(`{"events":[{"identity":"assets/1/events/2"},{"identity":"assets/1/events/3"}]}`),
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := eventListFromData(test.args.data)

assert.Equal(t, test.wantErr, err != nil)
assert.Equal(t, test.expected, actual)
})
}
}
Loading
Loading