initial commit for APIDQuota
diff --git a/api.go b/api.go new file mode 100644 index 0000000..50d340f --- /dev/null +++ b/api.go
@@ -0,0 +1,62 @@ +package apidQuota + +import ( + "encoding/json" + "github.com/30x/apid-core" + "github.com/30x/apidQuota/quotaBucket" + "io/ioutil" + "net/http" + "strconv" +) + +func InitAPI(services apid.Services) { + Log.Debug("initialized API's exposed by apidQuota plugin") + quotaBasePath := Config.GetString(ConfigQuotaBasePath) + services.API().HandleFunc(quotaBasePath, getAllQuotaValues).Methods("GET") //yet to implement. + services.API().HandleFunc(quotaBasePath+"/{quotaItentifier}", incrementAndCheckQuotaLimit).Methods("POST") + +} + +func getAllQuotaValues(res http.ResponseWriter, req *http.Request) { + stringbytes := []byte("yet to implement") + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(stringbytes) + +} + +func incrementAndCheckQuotaLimit(res http.ResponseWriter, req *http.Request) { + + bodyBytes, err := ioutil.ReadAll(req.Body) + defer req.Body.Close() + if err != nil { + WriteErrorResponse(http.StatusBadRequest, UnableToParseBody, "unable to read request body: "+err.Error(), res, req) + return + } + + quotaBucketMap := make(map[string]interface{}, 0) + if err := json.Unmarshal(bodyBytes, "aBucketMap); err != nil { + WriteErrorResponse(http.StatusBadRequest, UnMarshalJSONError, "unable to convert request body to an object: "+err.Error(), res, req) + return + } + Log.Println("quotaBucketMap from request: ", quotaBucketMap) + + // parse the request body into the Event struct + qBucket := new(quotaBucket.QuotaBucket) + if err = qBucket.FromAPIRequest(quotaBucketMap); err != nil { + WriteErrorResponse(http.StatusBadRequest, ErrorConvertReqBodyToEntity, err.Error(), res, req) + return + } + + quotaCount, err := qBucket.GetQuotaCount() + if err != nil { + WriteErrorResponse(http.StatusBadRequest, UnMarshalJSONError, "error retrieving count for the give identifier "+err.Error(), res, req) + return + } + + stringbytes := []byte(strconv.Itoa(quotaCount)) + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(stringbytes) + +}
diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..588b808 --- /dev/null +++ b/constants.go
@@ -0,0 +1,9 @@ +package apidQuota + +const ( + UnableToParseBody = "unable_to_parse_body" + UnMarshalJSONError = "unmarshal_json_error" + ErrorConvertReqBodyToEntity = "error_convert_reqBody_to_entity" + ConfigQuotaBasePath = "quota_base_path" + quotaBasePathDefault = "/quota" +)
diff --git a/init.go b/init.go index 7ec68f9..93e1c1b 100644 --- a/init.go +++ b/init.go
@@ -4,9 +4,9 @@ "github.com/30x/apid-core" ) -var( - log apid.LogService - +var ( + Log apid.LogService + Config apid.ConfigService ) func init() { @@ -14,8 +14,19 @@ } func initPlugin(services apid.Services) (apid.PluginData, error) { - log = services.Log().ForModule("apidQuota") - log.Debug("start init") + Log = services.Log().ForModule("apidQuota") + Log.Debug("start init") + + setConfig(services) + InitAPI(services) return pluginData, nil } + +func setConfig(services apid.Services) { + // set configuration + Config = services.Config() + // set plugin config defaults + Config.SetDefault(ConfigQuotaBasePath, quotaBasePathDefault) + +}
diff --git a/quotaBucket/QuotaDescriptorType.go b/quotaBucket/QuotaDescriptorType.go new file mode 100644 index 0000000..3e15c4a --- /dev/null +++ b/quotaBucket/QuotaDescriptorType.go
@@ -0,0 +1,119 @@ +package quotaBucket + +import ( + "errors" + "fmt" + "strings" + "time" +) + +const ( + QuotaTypeCalendar = "calendar" // after start time + QuotaTypeFlexi = "flexi" //after first request + QuotaTypeRollingWindow = "rollingWindow" // in the past "window" time +) + +type QuotaDescriptorType interface { + SetCurrentPeriod(bucket *QuotaBucket) error +} + +func GetQuotaTypeHandler(qType string) (QuotaDescriptorType, error) { + var qDescriptor QuotaDescriptorType + quotaType := strings.ToLower(strings.TrimSpace(qType)) + switch quotaType { + case QuotaTypeCalendar: + qDescriptor = &CanlendarQuotaDescriporType{} + return qDescriptor, nil + case QuotaTypeFlexi: + qDescriptor = &FlexiQuotaDescriptorType{} + return qDescriptor, nil + case QuotaTypeRollingWindow: + qDescriptor = &RollingWindowQuotaDescriptorType{} + return qDescriptor, nil + default: + return nil, errors.New("Ignoring unrecognized quota type in request: " + qType) + + } +} + +type CanlendarQuotaDescriporType struct{} + +func (c CanlendarQuotaDescriporType) SetCurrentPeriod(qbucket *QuotaBucket) error { + var err error + startTime := qbucket.GetStartTime() + currentPeriod, err := qbucket.GetPeriod() + if err != nil { + return err + } + + if startTime.Before(time.Now()) || startTime.Equal(time.Now()) { + if currentPeriod != nil { + if currentPeriod.IsCurrentPeriod(qbucket) { + return nil + } + } else { + if currentPeriod.IsCurrentPeriod(qbucket) { + return nil + } else { + qBucketHandler, err := GetQuotaBucketHandler(qbucket.BucketType) + if err != nil { + return errors.New("error getting QuotaBucketType: " + err.Error()) + } + qBucketHandler.resetCount(qbucket) + } + } + } + + var currentStart, currentEnd time.Time + now := time.Now() + timeUnit := strings.ToLower(strings.TrimSpace(qbucket.TimeUnit)) + switch timeUnit { + case TimeUnitSECOND: + currentStart = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), 0, time.UTC) + secInDuration := time.Duration(int64(qbucket.Interval) * time.Second.Nanoseconds()) + currentEnd = currentStart.Add(secInDuration) + break + case TimeUnitMINUTE: + currentStart = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC) + minInDuration := time.Duration(int64(qbucket.Interval) * time.Minute.Nanoseconds()) + currentEnd = currentStart.Add(minInDuration) + break + case TimeUnitHOUR: + currentStart = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.UTC) + hoursInDuration := time.Duration(int64(qbucket.Interval) * time.Hour.Nanoseconds()) + currentEnd = currentStart.Add(hoursInDuration) + + break + case TimeUnitDAY: + currentStart = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + currentEnd = currentStart.AddDate(0, 0, 1*qbucket.Interval) + break + case TimeUnitWEEK: + //todo + //currentStart = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + //currentEnd = currentStart.AddDate(0, 0, 7*qbucket.Interval) + break + case TimeUnitMONTH: + currentStart = time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.UTC) + currentEnd = currentStart.AddDate(0, 1*qbucket.Interval, 0) + break + } + + qbucket.SetPeriod(currentStart, currentEnd) + fmt.Println("inside calendat set period: ", qbucket.quotaBucketData.Period) + return nil +} + +type FlexiQuotaDescriptorType struct{} + +func (c FlexiQuotaDescriptorType) SetCurrentPeriod(qbucket *QuotaBucket) error { + //yet to implement + return nil +} + +type RollingWindowQuotaDescriptorType struct{} + +func (c RollingWindowQuotaDescriptorType) SetCurrentPeriod(qbucket *QuotaBucket) error { + //yet to implement + return nil +}
diff --git a/quotaBucket/apiUtil.go b/quotaBucket/apiUtil.go new file mode 100644 index 0000000..c00a879 --- /dev/null +++ b/quotaBucket/apiUtil.go
@@ -0,0 +1,173 @@ +package quotaBucket + +import ( + "errors" + "fmt" + "reflect" +) + +func (qBucket *QuotaBucket) FromAPIRequest(quotaBucketMap map[string]interface{}) error { + var edgeOrgID, id, timeUnit, quotaType, bucketType string + var interval, maxCount int + var startTime int64 + var preciseAtSecondsLevel bool + + fmt.Println("quotaBucketMap: ", quotaBucketMap) + + value, ok := quotaBucketMap["edgeOrgID"] + if !ok { + return errors.New(`missing field: 'edgeOrgID' is required`) + } + if edgeOrgIDType := reflect.TypeOf(value); edgeOrgIDType.Kind() != reflect.String { + return errors.New(`invalid type : 'edgeOrgID' should be a string`) + } + edgeOrgID = value.(string) + //fmt.Println("edgeOrgID: ", edgeOrgID) + + value, ok = quotaBucketMap["id"] + if !ok { + return errors.New(`missing field: 'id' is required`) + } + if idType := reflect.TypeOf(value); idType.Kind() != reflect.String { + return errors.New(`invalid type : 'id' should be a string`) + } + id = value.(string) + //fmt.Println("id: ", id) + + value, ok = quotaBucketMap["interval"] + if !ok { + return errors.New(`missing field: 'interval' is required`) + } + //from input when its read its float, need to then convert to int. + if intervalType := reflect.TypeOf(value); intervalType.Kind() != reflect.Float64 { + return errors.New(`invalid type : 'interval' should be a number`) + } + intervalFloat := value.(float64) + interval = int(intervalFloat) + //fmt.Println("interval: ", interval) + + //TimeUnit {SECOND, MINUTE, HOUR, DAY, WEEK, MONTH} + value, ok = quotaBucketMap["timeUnit"] + if !ok { + return errors.New(`missing field: 'timeUnit' is required`) + } + if timeUnitType := reflect.TypeOf(value); timeUnitType.Kind() != reflect.String { + return errors.New(`invalid type : 'timeUnit' should be a string`) + } + timeUnit = value.(string) + //fmt.Println("timeUnit: ", timeUnit) + + //Type {CALENDAR, FLEXI, ROLLING_WINDOW} + value, ok = quotaBucketMap["quotaType"] + if !ok { + return errors.New(`missing field: 'quotaType' is required`) + } + if quotaTypeType := reflect.TypeOf(value); quotaTypeType.Kind() != reflect.String { + return errors.New(`invalid type : 'quotaType' should be a string`) + } + quotaType = value.(string) + //fmt.Println("quotaType: ", quotaType) + + value, ok = quotaBucketMap["preciseAtSecondsLevel"] + if !ok { + return errors.New(`missing field: 'preciseAtSecondsLevel' is required`) + } + if preciseAtSecondsLevelType := reflect.TypeOf(value); preciseAtSecondsLevelType.Kind() != reflect.Bool { + return errors.New(`invalid type : 'preciseAtSecondsLevel' should be boolean`) + } + preciseAtSecondsLevel = value.(bool) + //fmt.Println("preciseAtSecondsLevel: ", preciseAtSecondsLevel) + + value, ok = quotaBucketMap["startTime"] + if !ok { + return errors.New(`missing field: 'startTime' is required`) + } + //from input when its read its float, need to then convert to int. + if startTimeType := reflect.TypeOf(value); startTimeType.Kind() != reflect.Float64 { + return errors.New(`invalid type : 'startTime' should be UNIX timestamp`) + } + startTimeFloat := value.(float64) + startTime = int64(startTimeFloat) + //fmt.Println("startTime: ", startTime) + + quotaPeriod := QuotaPeriod{} + value, ok = quotaBucketMap["period"] + //if period is not sent in the request, it is calculated based in the startTime, quotaType and interval. + if ok { + var inStartInt, startInt, endInt int64 + + isPeriodMap := reflect.TypeOf(value) + if isPeriodMap.Kind() != reflect.Map { + return errors.New(`invalid type : 'period' should be a Map`) + } + periodMap := value.(map[string]interface{}) + + inStartTimeValue, ok := periodMap["inputStartTime"] + if !ok { + //set period.inputStart time from qBucket.startTime + inStartInt = startTime + } else { + if inStartType := reflect.TypeOf(inStartTimeValue); inStartType.Kind() != reflect.Float64 { + return errors.New(`invalid type : 'inputStartTime' in 'period' should be UNIX timestamp`) + } + inStartFloat := inStartTimeValue.(float64) + inStartInt = int64(inStartFloat) + if startTime != inStartInt { + return errors.New(`invalid value : 'inputStartTime' in 'period' should be same as 'startTime'' in request`) + } + } + + startTimeValue, ok := periodMap["startTime"] + if !ok { + return errors.New(`missing field : 'startTime' in 'period' cannot be empty`) + } + if periodStartType := reflect.TypeOf(startTimeValue); periodStartType.Kind() != reflect.Float64 { + return errors.New(`invalid type : 'startTime' in 'period' should be UNIX timestamp`) + } + periodStartFloat := startTimeValue.(float64) + startInt = int64(periodStartFloat) + + periodEndValue, ok := periodMap["endTime"] + if !ok { + return errors.New(`missing field : 'endTime' in 'period' cannot be empty`) + } + if periodEndType := reflect.TypeOf(periodEndValue); periodEndType.Kind() != reflect.Float64 { + return errors.New(`invalid type : 'endTime' in 'period' should be UNIX timestamp`) + } + periodEndFloat := periodEndValue.(float64) + endInt = int64(periodEndFloat) + + quotaPeriod = NewQuotaPeriod(inStartInt, startInt, endInt) + } + + value, ok = quotaBucketMap["maxCount"] + if !ok { + return errors.New(`missing field: 'maxCount' is required`) + } + //from input when its read its float, need to then convert to int. + if maxCountType := reflect.TypeOf(value); maxCountType.Kind() != reflect.Float64 { + return errors.New(`invalid type : 'maxCount' should be a number`) + } + maxCountFloat := value.(float64) + maxCount = int(maxCountFloat) + //fmt.Println("maxCount: ", maxCount) + + value, ok = quotaBucketMap["bucketType"] + if !ok { + return errors.New(`missing field: 'bucketType' is required`) + } + if bucketTypeType := reflect.TypeOf(value); bucketTypeType.Kind() != reflect.String { + return errors.New(`invalid type : 'bucketType' should be a string`) + } + bucketType = value.(string) + //fmt.Println("bucketType: ", bucketType) + + newQBucket, err := NewQuotaBucket(edgeOrgID, id, interval, timeUnit, quotaType, preciseAtSecondsLevel, quotaPeriod, startTime, maxCount, bucketType) + if err != nil { + return errors.New("unable to Unmarshal periodMap to quotaPeriod: " + err.Error()) + + } + qBucket.quotaBucketData = newQBucket.quotaBucketData + return nil + +}
diff --git a/quotaBucket/quotaBucket.go b/quotaBucket/quotaBucket.go new file mode 100644 index 0000000..36c9082 --- /dev/null +++ b/quotaBucket/quotaBucket.go
@@ -0,0 +1,169 @@ +package quotaBucket + +import ( + "errors" + "fmt" + "time" +) + +const ( + TimeUnitSECOND = "second" + TimeUnitMINUTE = "minute" + TimeUnitHOUR = "hour" + TimeUnitDAY = "day" + TimeUnitWEEK = "week" + TimeUnitMONTH = "month" +) + +type QuotaPeriod struct { + inputStartTime time.Time + startTime time.Time + endTime time.Time +} + +func NewQuotaPeriod(inputStartTime int64, startTime int64, endTime int64) QuotaPeriod { + pInStartTime := time.Unix(inputStartTime, 0) + pStartTime := time.Unix(startTime, 0) + pEndTime := time.Unix(endTime, 0) + + period := &QuotaPeriod{inputStartTime: pInStartTime, + startTime: pStartTime, + endTime: pEndTime, + } + return *period + +} + +type quotaBucketData struct { + EdgeOrgID string + ID string + Interval int + TimeUnit string //TimeUnit {SECOND, MINUTE, HOUR, DAY, WEEK, MONTH} + QuotaType string //Type {CALENDAR, FLEXI, ROLLING_WINDOW} + PreciseAtSecondsLevel bool + Period QuotaPeriod + StartTime time.Time + MaxCount int + BucketType string // SyncDistributed, AsyncDistributed, NonDistributed +} + +type QuotaBucket struct { + quotaBucketData +} + +func NewQuotaBucket(edgeOrgID string, id string, interval int, + timeUnit string, quotaType string, preciseAtSecondsLevel bool, period QuotaPeriod, + startTime int64, maxCount int, bucketType string) (*QuotaBucket, error) { + + fromUNIXTime := time.Unix(startTime, 0) + + quotaBucketDataStruct := "aBucketData{ + EdgeOrgID: edgeOrgID, + ID: id, + Interval: interval, + TimeUnit: timeUnit, + QuotaType: quotaType, + PreciseAtSecondsLevel: preciseAtSecondsLevel, + Period: period, + StartTime: fromUNIXTime, + MaxCount: maxCount, + BucketType: bucketType, + } + + quotaBucket := &QuotaBucket{ + quotaBucketData: *quotaBucketDataStruct, + } + + return quotaBucket, nil + +} + +func (q *QuotaBucket) GetEdgeOrgID() string { + return q.quotaBucketData.EdgeOrgID +} + +func (q *QuotaBucket) GetID() string { + return q.quotaBucketData.ID +} + +func (q *QuotaBucket) GetInterval() int { + return q.quotaBucketData.Interval +} + +func (q *QuotaBucket) GetStartTime() time.Time { + return q.quotaBucketData.StartTime +} + +func (q *QuotaBucket) GetQuotaType() string { + return q.quotaBucketData.QuotaType +} + +func (q *QuotaBucket) GetPeriod() (*QuotaPeriod, error) { + if q.quotaBucketData.QuotaType == QuotaTypeRollingWindow { + qRWType := RollingWindowQuotaDescriptorType{} + err := qRWType.SetCurrentPeriod(q) + if err != nil { + return nil, err + } + } + return &q.quotaBucketData.Period, nil +} + +func (q *QuotaBucket) GetMaxCount() int { + return q.quotaBucketData.MaxCount +} + +func (q *QuotaBucket) GetBucketType() string { + return q.quotaBucketData.BucketType +} + +func (q *quotaBucketData) SetPeriod(startTime time.Time, endTime time.Time) { + q.Period = QuotaPeriod{inputStartTime: q.StartTime, + startTime: startTime, + endTime: endTime, + } +} + +func (q *QuotaBucket) setCurrentPeriod() error { + + qDescriptorType, err := GetQuotaTypeHandler(q.GetQuotaType()) + if err != nil { + return err + } + return qDescriptorType.SetCurrentPeriod(q) + +} + +func (period *QuotaPeriod) IsCurrentPeriod(qBucket *QuotaBucket) bool { + if qBucket != nil && qBucket.GetBucketType() != "" { + if qBucket.GetBucketType() == QuotaTypeRollingWindow { + return (period.inputStartTime.Equal(time.Now()) || period.inputStartTime.Before(time.Now())) + } + return ((period.inputStartTime.Equal(time.Now()) || period.inputStartTime.Before(time.Now())) && + period.startTime.String() != "" && period.endTime.String() != "" && + period.startTime.Before(time.Now()) && period.startTime.Equal(time.Now()) && + period.endTime.Before(time.Now()) && period.startTime.Equal(time.Now())) + } + return false +} + +func (q *QuotaBucket) GetQuotaCount() (int, error) { + err := q.setCurrentPeriod() + if err != nil { + return 0, errors.New("error setCurrentPeriod(): " + err.Error()) + } + period, err := q.GetPeriod() + if err != nil { + return 0, errors.New("error getting period: " + err.Error()) + } + fmt.Println("period set: ", period) + + //todo API call to counter service using Period start end and keyspace. + + return 10, nil +} + +func (q *QuotaBucket) IncrementQuota() (int, error) { + //todo + return 0, nil +}
diff --git a/quotaBucket/quotaBucketType.go b/quotaBucket/quotaBucketType.go new file mode 100644 index 0000000..843846b --- /dev/null +++ b/quotaBucket/quotaBucketType.go
@@ -0,0 +1,57 @@ +package quotaBucket + +import ( + "errors" + "strings" +) + +const ( + QuotaBucketTypeSynchronous = "synchronous" + QuotaBucketTypeAsynchronous = "asynchronous" + QuotaBucketTypeNonDistributed = "nonDistributed" + //todo: Add other bucketTypes +) + +type QuotaBucketType interface { + resetCount(bucket *QuotaBucket) error +} + +type SynchronousQuotaBucketType struct{} + +func (sQuotaBucket SynchronousQuotaBucketType) resetCount(qBucket *QuotaBucket) error { + //do nothing. + return nil +} + +type AsynchronousQuotaBucketType struct{} + +func (sQuotaBucket AsynchronousQuotaBucketType) resetCount(qBucket *QuotaBucket) error { + //yet to implement + return nil +} + +type NonDistributedQuotaBucketType struct{} + +func (sQuotaBucket NonDistributedQuotaBucketType) resetCount(qBucket *QuotaBucket) error { + //yet to implement + return nil +} + +func GetQuotaBucketHandler(qBucket string) (QuotaBucketType, error) { + var quotaBucketType QuotaBucketType + qBucketType := strings.ToLower(strings.TrimSpace(qBucket)) + switch qBucketType { + case QuotaBucketTypeSynchronous: + quotaBucketType = &SynchronousQuotaBucketType{} + return quotaBucketType, nil + case QuotaBucketTypeAsynchronous: + quotaBucketType = &AsynchronousQuotaBucketType{} + return quotaBucketType, nil + case QuotaBucketTypeNonDistributed: + quotaBucketType = &NonDistributedQuotaBucketType{} + return quotaBucketType, nil + default: + return nil, errors.New("Ignoring unrecognized quota type in request: " + qBucket) + + } +}
diff --git a/util.go b/util.go new file mode 100644 index 0000000..696d5bc --- /dev/null +++ b/util.go
@@ -0,0 +1,20 @@ +package apidQuota + +import ( + "encoding/json" + "net/http" +) + +// WriteErrorResponse will write the HTTP header and payload for the HTTP ResponseWriter provided +func WriteErrorResponse(status int, errorType string, errorDescription string, res http.ResponseWriter, req *http.Request) { + response := make(map[string]string) + response["error"] = errorType + response["errorDescription"] = errorDescription + responseJson, err := json.Marshal(response) + if err != nil { + panic(err) + } + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(status) + res.Write(responseJson) +}