Skip to content

Commit

Permalink
added initial support for lazy fetch definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastian Mancke committed Jan 31, 2017
1 parent e2c6fb3 commit 30620d3
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 45 deletions.
49 changes: 36 additions & 13 deletions composition/content_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import (
"sync"
)

// IsFetchable returns, whether the fetch definition refers to a fetchable resource
// or is a local name only.
func (def *FetchDefinition) IsFetchable() bool {
return len(def.URL) > 0
}

type FetchResult struct {
Def *FetchDefinition
Err error
Expand All @@ -33,18 +27,22 @@ func (fr FetchResults) Less(i, j int) bool {
return fr[i].Def.Priority < fr[j].Def.Priority
}

type FetchDefinitionFactory func(name string, params Params) (fd *FetchDefinition, exist bool, err error)

// ContentFetcher is a type, which can fetch a set of Content pages in parallel.
type ContentFetcher struct {
activeJobs sync.WaitGroup
r struct {
results []*FetchResult
mutex sync.Mutex
sheduledFetchDefinitionNames map[string]string
results []*FetchResult
mutex sync.Mutex
}
meta struct {
json map[string]interface{}
mutex sync.Mutex
}
Loader ContentLoader
lazyFdFactory FetchDefinitionFactory
Loader ContentLoader
}

// NewContentFetcher creates a ContentFetcher with an HtmlContentParser as default.
Expand All @@ -53,14 +51,22 @@ type ContentFetcher struct {
func NewContentFetcher(defaultMetaJSON map[string]interface{}) *ContentFetcher {
f := &ContentFetcher{}
f.r.results = make([]*FetchResult, 0, 0)
f.r.sheduledFetchDefinitionNames = make(map[string]string)
f.Loader = NewHttpContentLoader()
f.meta.json = defaultMetaJSON
if f.meta.json == nil {
f.meta.json = make(map[string]interface{})
}
f.lazyFdFactory = func(name string, params Params) (fd *FetchDefinition, exist bool, err error) {
return nil, false, nil
}
return f
}

func (fetcher *ContentFetcher) SetFetchDefinitionFactory(factory FetchDefinitionFactory) {
fetcher.lazyFdFactory = factory
}

// Wait blocks until all jobs are done,
// either successful or with an error result and returns the content and errors.
// Do we need to return the Results in a special order????
Expand All @@ -80,6 +86,8 @@ func (fetcher *ContentFetcher) WaitForResults() []*FetchResult {
return results
}

//func (fetcher *ContentFetcher) AddFetchDefinitionFactory(name string, func(params map[string]string) *FetchDefinition) {

// AddFetchJob adds one job to the fetcher and recursively adds the dependencies also.
func (fetcher *ContentFetcher) AddFetchJob(d *FetchDefinition) {
fetcher.r.mutex.Lock()
Expand All @@ -91,9 +99,9 @@ func (fetcher *ContentFetcher) AddFetchJob(d *FetchDefinition) {
}

fetcher.activeJobs.Add(1)

fetchResult := &FetchResult{Def: d, Hash: hash, Err: errors.New("not fetched")}
fetcher.r.results = append(fetcher.r.results, fetchResult)
fetcher.r.sheduledFetchDefinitionNames[d.Name] = d.Name

go func() {
defer fetcher.activeJobs.Done()
Expand All @@ -115,9 +123,24 @@ func (fetcher *ContentFetcher) AddFetchJob(d *FetchDefinition) {

if fetchResult.Err == nil {
fetcher.addMeta(fetchResult.Content.Meta())
for _, dependency := range fetchResult.Content.RequiredContent() {
if dependency.IsFetchable() {
fetcher.AddFetchJob(dependency)
for _, fetch := range fetchResult.Content.RequiredContent() {
fetcher.AddFetchJob(fetch)
}
for dependencyName, params := range fetchResult.Content.Dependencies() {
_, exist := fetcher.r.sheduledFetchDefinitionNames[dependencyName]
if !exist {
lazyFd, exist, err := fetcher.lazyFdFactory(dependencyName, params)
if err != nil {
logging.Logger.WithError(err).
WithField("dependencyName", dependencyName).
WithField("params", params).
Errorf("failed fetching dependency %v", dependencyName)
}
if err == nil && exist {
fetcher.AddFetchJob(lazyFd)
}
// error handling: In the case, the fd could not be loaded, we will do
// the error handling in the merging process.
}
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions composition/content_fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ func getFetchDefinitionMock(ctrl *gomock.Controller, loaderMock *MockContentLoad
Meta().
Return(metaJSON)

content.EXPECT().
Dependencies().
Return(map[string]Params{})

loaderMock.EXPECT().
Load(fd).
Do(
Expand Down
50 changes: 31 additions & 19 deletions composition/html_content_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import (
)

const (
UicRemove = "uic-remove"
UicInclude = "uic-include"
UicFetch = "uic-fetch"
UicFragment = "uic-fragment"
UicTail = "uic-tail"
ScriptTypeMeta = "text/uic-meta"
UicRemove = "uic-remove"
UicInclude = "uic-include"
UicFetch = "uic-fetch"
UicFragment = "uic-fragment"
UicTail = "uic-tail"
ScriptTypeMeta = "text/uic-meta"
ParamAttrPrefix = "param-"
)

type HtmlContentParser struct {
Expand Down Expand Up @@ -123,8 +124,8 @@ forloop:
return err
} else {
c.body[getFragmentName(attrs)] = f
for _, dep := range deps {
c.requiredContent[dep.URL] = dep
for depName, depParams := range deps {
c.dependencies[depName] = depParams
}
}
continue
Expand All @@ -134,8 +135,8 @@ forloop:
return err
} else {
c.tail = f
for _, dep := range deps {
c.requiredContent[dep.URL] = dep
for depName, depParams := range deps {
c.dependencies[depName] = depParams
}
}
continue
Expand All @@ -149,9 +150,10 @@ forloop:
}
}
if string(tag) == UicInclude {
if replaceTextStart, replaceTextEnd, err := getInclude(z, attrs); err != nil {
if replaceTextStart, replaceTextEnd, dependencyName, dependencyParams, err := getInclude(z, attrs); err != nil {
return err
} else {
c.dependencies[dependencyName] = dependencyParams
bodyBuff.WriteString(replaceTextStart)
// Enhancement: WriteOut sub tree, to allow alternative content
// for optional includes.
Expand All @@ -178,9 +180,9 @@ forloop:
return nil
}

func parseFragment(z *html.Tokenizer) (f Fragment, dependencies []*FetchDefinition, err error) {
func parseFragment(z *html.Tokenizer) (f Fragment, dependencies map[string]Params, err error) {
attrs := make([]html.Attribute, 0, 10)
dependencies = make([]*FetchDefinition, 0, 0)
dependencies = make(map[string]Params)

buff := bytes.NewBuffer(nil)
forloop:
Expand All @@ -198,9 +200,10 @@ forloop:
break forloop
case tt == html.StartTagToken || tt == html.SelfClosingTagToken:
if string(tag) == UicInclude {
if replaceTextStart, replaceTextEnd, err := getInclude(z, attrs); err != nil {
if replaceTextStart, replaceTextEnd, dependencyName, dependencyParams, err := getInclude(z, attrs); err != nil {
return nil, nil, err
} else {
dependencies[dependencyName] = dependencyParams
fmt.Fprintf(buff, replaceTextStart)
// Enhancement: WriteOut sub tree, to allow alternative content
// for optional includes.
Expand All @@ -224,30 +227,39 @@ forloop:
return StringFragment(buff.String()), dependencies, nil
}

func getInclude(z *html.Tokenizer, attrs []html.Attribute) (startMarker, endMarker string, error error) {
func getInclude(z *html.Tokenizer, attrs []html.Attribute) (startMarker, endMarker, dependencyName string, dependencyParams Params, error error) {
var srcString string
if url, hasUrl := getAttr(attrs, "src"); !hasUrl {
return "", "", fmt.Errorf("include definition without src %s", z.Raw())
return "", "", "", nil, fmt.Errorf("include definition without src %s", z.Raw())
} else {
srcString = strings.TrimSpace(url.Val)
if strings.HasPrefix(srcString, "#") {
srcString = srcString[1:]
}
dependencyName = strings.Split(srcString, "#")[0]
}

dependencyParams = Params{}
for _, a := range attrs {
if strings.HasPrefix(a.Key, ParamAttrPrefix) {
key := a.Key[len(ParamAttrPrefix):]
dependencyParams[key] = a.Val
}
}

required := false
if r, hasRequired := getAttr(attrs, "required"); hasRequired {
if requiredBool, err := strconv.ParseBool(r.Val); err != nil {
return "", "", fmt.Errorf("error parsing bool in %s: %s", z.Raw(), err.Error())
return "", "", "", nil, fmt.Errorf("error parsing bool in %s: %s", z.Raw(), err.Error())
} else {
required = requiredBool
}
}

if required {
return fmt.Sprintf("§[> %s]§", srcString), "", nil
return fmt.Sprintf("§[> %s]§", srcString), "", dependencyName, dependencyParams, nil
} else {
return fmt.Sprintf("§[#> %s]§", srcString), fmt.Sprintf("§[/%s]§", srcString), nil
return fmt.Sprintf("§[#> %s]§", srcString), fmt.Sprintf("§[/%s]§", srcString), dependencyName, dependencyParams, nil
}
}

Expand Down
10 changes: 10 additions & 0 deletions composition/interface_mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ func (_mr *_MockContentRecorder) BodyAttributes() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "BodyAttributes")
}

func (_m *MockContent) Dependencies() map[string]Params {
ret := _m.ctrl.Call(_m, "Dependencies")
ret0, _ := ret[0].(map[string]Params)
return ret0
}

func (_mr *_MockContentRecorder) Dependencies() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Dependencies")
}

func (_m *MockContent) Head() Fragment {
ret := _m.ctrl.Call(_m, "Head")
ret0, _ := ret[0].(Fragment)
Expand Down
7 changes: 7 additions & 0 deletions composition/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ type CacheStrategy interface {
IsCacheable(method string, url string, statusCode int, requestHeader http.Header, responseHeader http.Header) bool
}

// Params is a value type for a parameter map
type Params map[string]string

// Vontent is the abstration over includable data.
// Content may be parsed of it may contain a stream represented by a non nil Reader(), not both.
type Content interface {
Expand All @@ -54,6 +57,10 @@ type Content interface {
// RequiredContent returns a list of Content Elements to load
RequiredContent() []*FetchDefinition

// Dependencies returns list of referenced content element names.
// The list only contains the base names of the includes e.g. 'foo' for '<uic-include src="foo#bar"/>'
Dependencies() map[string]Params

// Meta returns a data structure to add to the global
// data context.
Meta() map[string]interface{}
Expand Down
6 changes: 6 additions & 0 deletions composition/memory_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
type MemoryContent struct {
name string
requiredContent map[string]*FetchDefinition // key ist the url
dependencies map[string]Params
meta map[string]interface{}
head Fragment
body map[string]Fragment
Expand All @@ -21,6 +22,7 @@ type MemoryContent struct {
func NewMemoryContent() *MemoryContent {
return &MemoryContent{
requiredContent: make(map[string]*FetchDefinition),
dependencies: make(map[string]Params),
meta: make(map[string]interface{}),
body: make(map[string]Fragment),
}
Expand Down Expand Up @@ -55,6 +57,10 @@ func (c *MemoryContent) RequiredContent() []*FetchDefinition {
return deps
}

func (c *MemoryContent) Dependencies() map[string]Params {
return c.dependencies
}

func (c *MemoryContent) Meta() map[string]interface{} {
return c.meta
}
Expand Down
16 changes: 3 additions & 13 deletions composition_example/example_ui_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,9 @@ func compositionHandler() http.Handler {
fetcher := composition.NewContentFetcher(defaultMetaJSON)
baseUrl := "http://" + r.Host

fetcher.AddFetchJob(&composition.FetchDefinition{
URL: baseUrl + "/static/layout.html",
Name: "layout",
})

fetcher.AddFetchJob(&composition.FetchDefinition{
URL: baseUrl + "/static/lorem.html",
Name: "content",
})

fetcher.AddFetchJob(&composition.FetchDefinition{
URL: baseUrl + "/static/styles.html",
})
fetcher.AddFetchJob(composition.NewFetchDefinition(baseUrl + "/static/layout.html").WithName("layout"))
fetcher.AddFetchJob(composition.NewFetchDefinition(baseUrl + "/static/lorem.html").WithName("content"))
fetcher.AddFetchJob(composition.NewFetchDefinition(baseUrl + "/static/styles.html"))

return fetcher
}
Expand Down

0 comments on commit 30620d3

Please sign in to comment.