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

Refactor/tag info codegen #169

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "pkg/tag/dicom-standard"]
path = pkg/tag/dicom-standard
url = https://github.com/innolitics/dicom-standard.git
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
BINARY = dicomutil
VERSION = `git describe --tags --always`

.PHONY: codegen
codegen:
- go generate -x ./...
- gofmt -s -w ./pkg/tag

.PHONY: build
build:
go mod download
Expand Down
4,351 changes: 4,351 additions & 0 deletions pkg/tag/attributes.go

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions pkg/tag/codegen/codegenWriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package main

import (
"fmt"
"io"
)

const packageName = "package tag\n\n"
const codegenWarning = "// Code generated from './pkg/tag/codegen'. " +
"DO NOT EDIT.\n\n"

// GeneratedFileLeader contains the preamble needed for a generated .go file in the
// tag package.
const GeneratedFileLeader = packageName + codegenWarning

// CodeWriter is an interface for writing to a dicom tag codegen file.
type CodeWriter interface {
// Name to use in error messages related to this writer.
Name() string

// WriteLeading writes the opening part of a file. Called once before calls to
// WriteTag() are made.
WriteLeading() error

// WriteTag writes codegen for a given tag. It may be called many times between
// WriteLeading and WriteTrailing. WriteTag should return io.EOF when all tags
// are successfully written.
WriteTag(info TagInfo) error

// WriteTrailing writes all codegen after WriteTag calls are complete.
WriteTrailing() error

// Close closes any underlying open resources.
Close() error
}

// MasterCodeWriter takes in multiple CodeWriter instances and exposes them as a single
// CodeWriter.
type MasterCodeWriter struct {
codegenWriters []CodeWriter
}

// Name implements CodeWriter, and returns "master codegen writer".
func (writer *MasterCodeWriter) Name() string {
return "master codegen writer"
}

// WriteLeading implements CodeWriter, and calls WriteLeading on all child code writers.
func (writer *MasterCodeWriter) WriteLeading() error {
for _, thisWriter := range writer.codegenWriters {
err := thisWriter.WriteLeading()
if err != nil {
return fmt.Errorf("error writing for %v: %w", thisWriter.Name(), err)
}
}

return nil
}

// WriteTag implements CodeWriter, and calls WriteTag with info on all child code
// writers.
func (writer *MasterCodeWriter) WriteTag(info TagInfo) error {
for _, thisWriter := range writer.codegenWriters {
err := thisWriter.WriteTag(info)
if err != nil {
return fmt.Errorf("error writing for %v: %w", thisWriter.Name(), err)
}
}

return nil
}

// WriteTrailing implements CodeWriter, and calls WriteTrailing and all child readers.
func (writer *MasterCodeWriter) WriteTrailing() error {
for _, thisWriter := range writer.codegenWriters {
err := thisWriter.WriteTrailing()
if err != nil {
return fmt.Errorf("error writing for %v: %w", thisWriter.Name(), err)
}
}

return nil
}

// Close implements CodeWriter, and closes all child readers.
func (writer *MasterCodeWriter) Close() (err error) {
for _, thisWriter := range writer.codegenWriters {
defer func(closer io.Closer) {
thisErr := closer.Close()
if thisErr != nil && err == nil {
err = thisErr
}
}(thisWriter)
}

return err
}

// NewMasterCodeWriter creates a MasterCodeWriter from a slice of functions that
// create new child writers for the MasterCodeWriter to handle.
func NewMasterCodeWriter(
codegenWriterCreators []func() (CodeWriter, error),
) (writer CodeWriter, err error) {
masterWriter := &MasterCodeWriter{
codegenWriters: make([]CodeWriter, 0, len(codegenWriterCreators)),
}

// Close master writer (and all sub-writers) on error.
defer func() {
if err != nil {
masterWriter.Close()
}
}()

var thisWriter CodeWriter
for _, thisCreator := range codegenWriterCreators {
thisWriter, err = thisCreator()
if err != nil {
return nil, err
}

masterWriter.codegenWriters = append(masterWriter.codegenWriters, thisWriter)
}

return masterWriter, nil
}
65 changes: 65 additions & 0 deletions pkg/tag/codegen/codegenWriterAttributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"os"
)

const attributesFilePath = "./attributes.go"

// AttributeCodeWriter writes codegen to attributes.go via CodegenWriter interface.
type AttributeCodeWriter struct {
attributesFileWriter *os.File
}

// Name implements CodeWriter, and returns "attribute var writer".
func (writer *AttributeCodeWriter) Name() string {
return "attribute var writer"
}

// WriteLeading implements CodeWriter, and writes the generic preamble to the attributes
// file.
func (writer *AttributeCodeWriter) WriteLeading() error {
// We just need to write the basics here, package name and generated comment.
_, err := writer.attributesFileWriter.WriteString(GeneratedFileLeader)
return err
}

// WriteTag implements CodeWriter, it writes a single var declaration for each tag
// passed to it.
func (writer *AttributeCodeWriter) WriteTag(info TagInfo) error {
// Each tag is a var declaration on a new line.
entry := fmt.Sprintf(
"var %v = Tag{0x%04x,0x%04x}\n",
info.Name,
info.Tag.Group,
info.Tag.Element,
)
_, err := writer.attributesFileWriter.WriteString(entry)
return err
}

// WriteTrailing implements CodeWriter, but does not write anything in this case.
func (writer *AttributeCodeWriter) WriteTrailing() error {
return nil
}

// Close implements CodeWriter, and closes the attributes file.
func (writer *AttributeCodeWriter) Close() (err error) {
return writer.attributesFileWriter.Close()
}

// NewAttributesCodeWriter creates a new AttributeCodeWriter for writing the
// attributes.go file.
func NewAttributesCodeWriter() (CodeWriter, error) {
fileWriter, err := os.Create(attributesFilePath)
if err != nil {
return nil, fmt.Errorf(
"error file '%v' for write: %w", attributesFilePath, err,
)
}

return &AttributeCodeWriter{
attributesFileWriter: fileWriter,
}, nil
}
86 changes: 86 additions & 0 deletions pkg/tag/codegen/codegenWriterDicts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"fmt"
"os"
)

const tagDictPath = "./dicts.go"

// TagDictCodeWriter writes codegen to dicts.go via CodeWriter interface.
type TagDictCodeWriter struct {
fileWriter *os.File
}

// Name implements CodeWriter, and returns "tag info dict writer".
func (writer *TagDictCodeWriter) Name() string {
return "tag info dict writer"
}

// WriteLeading implements CodeWriter, and writes the generic codegen preamble, as well
// as the var declarations for our dicts, and opens declaration of the
// function we will use to init them.
func (writer *TagDictCodeWriter) WriteLeading() error {
// Set up the two dicts and open the function we will use to init them.
declareDict := "var tagDict = make(map[Tag]Info)\n"
declareDict += "var tagDictByKeyword = make(map[string]Info)\n\n"
initDictOpen := "func initTagDicts() {\n\tvar thisInfo Info\n\n"

leader := GeneratedFileLeader + declareDict + initDictOpen

_, err := writer.fileWriter.WriteString(leader)
return err
}

// Each tag will generate the below code, loading the info into an existing variable
// and then inserting it into both maps.
const tagDeclarationTemplate = ` thisInfo = Info{
Tag: Tag{0x%04x,0x%04x},
Name: "%v",
VR: "%v",
VM: "%v",
}
tagDict[thisInfo.Tag] = thisInfo
tagDictByKeyword[thisInfo.Name] = thisInfo

`

// WriteTag implements CodeWriter, and adds each tag passed to it to each of our dicts.
func (writer *TagDictCodeWriter) WriteTag(info TagInfo) error {
// Insert our parsed values into the code template.
addToDicts := fmt.Sprintf(
tagDeclarationTemplate,
info.Tag.Group,
info.Tag.Element,
info.Name,
info.VR,
info.VM,
)

_, err := writer.fileWriter.WriteString(addToDicts)
return err
}

// WriteTrailing implements CodeWriter, and closes our dict init function.
func (writer *TagDictCodeWriter) WriteTrailing() error {
// Close initDicts() function
_, err := writer.fileWriter.WriteString("\n}\n")
return err
}

// Close implements CodeWriter, and closes the dicts.go file.
func (writer *TagDictCodeWriter) Close() error {
return writer.fileWriter.Close()
}

// NewTagDictCodeWriter creates a new TagDictCodeWriter for writing the dicts.go file.
func NewTagDictCodeWriter() (CodeWriter, error) {
fileWriter, err := os.Create(tagDictPath)
if err != nil {
return nil, fmt.Errorf("error opening '%v': %w", tagDictPath, err)
}

return &TagDictCodeWriter{
fileWriter: fileWriter,
}, nil
}
42 changes: 42 additions & 0 deletions pkg/tag/codegen/dataModels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"strconv"
"strings"
)

// Tag holds tag group and element.
type Tag struct {
// Group is the metadata group piece of the tag.
Group uint16
// Element is the metadata element identifier half of the tag.
Element uint16
}

// TagInfo is a mirror of tag.Info that we will parse our spec data into.
type TagInfo struct {
// Tag is the (XXXX,YYYY) metadata element identifier.
Tag Tag
// VR is the DICOM Value Representation string that indicates what type of data
// this tag holds.
VR string
// Name is the dicom machine readable Keyword associated with the tag.
Name string
// VM is the value multiplicity of the tag (how many VR values does / can the
// element store)
VM string
}

// ParseTag parses a Tag from a (XXXX,YYYY) string.
func ParseTag(tag string) (Tag, error) {
parts := strings.Split(strings.Trim(tag, "()"), ",")
group, err := strconv.ParseInt(parts[0], 16, 0)
if err != nil {
return Tag{}, err
}
elem, err := strconv.ParseInt(parts[1], 16, 0)
if err != nil {
return Tag{}, err
}
return Tag{Group: uint16(group), Element: uint16(elem)}, nil
}
24 changes: 24 additions & 0 deletions pkg/tag/codegen/dimse.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
(0000,0000) UL CommandGroupLength 1 DICOM_2011
(0000,0002) UI AffectedSOPClassUID 1 DICOM_2011
(0000,0003) UI RequestedSOPClassUID 1 DICOM_2011
(0000,0100) US CommandField 1 DICOM_2011
(0000,0110) US MessageID 1 DICOM_2011
(0000,0120) US MessageIDBeingRespondedTo 1 DICOM_2011
(0000,0600) AE MoveDestination 1 DICOM_2011
(0000,0700) US Priority 1 DICOM_2011
(0000,0800) US CommandDataSetType 1 DICOM_2011
(0000,0900) US Status 1 DICOM_2011
(0000,0901) AT OffendingElement 1-n DICOM_2011
(0000,0902) LO ErrorComment 1 DICOM_2011
(0000,0903) US ErrorID 1 DICOM_2011
(0000,1000) UI AffectedSOPInstanceUID 1 DICOM_2011
(0000,1001) UI RequestedSOPInstanceUID 1 DICOM_2011
(0000,1002) US EventTypeID 1 DICOM_2011
(0000,1005) AT AttributeIdentifierList 1-n DICOM_2011
(0000,1008) US ActionTypeID 1 DICOM_2011
(0000,1020) US NumberOfRemainingSuboperations 1 DICOM_2011
(0000,1021) US NumberOfCompletedSuboperations 1 DICOM_2011
(0000,1022) US NumberOfFailedSuboperations 1 DICOM_2011
(0000,1023) US NumberOfWarningSuboperations 1 DICOM_2011
(0000,1030) AE MoveOriginatorApplicationEntityTitle 1 DICOM_2011
(0000,1031) US MoveOriginatorMessageID 1 DICOM_2011
Loading