├── .ceylon ├── config └── config~ ├── .classpath ├── .gitignore ├── .project ├── .settings └── .gitignore ├── LICENSE ├── README.md ├── examples └── herd │ └── examples │ └── schedule │ └── chime │ ├── customtimer │ ├── customTimer.ceylon │ ├── package.ceylon │ └── run.ceylon │ ├── module.ceylon │ ├── package.ceylon │ └── run.ceylon ├── howto.md ├── scheduling.md ├── source └── herd │ └── schedule │ └── chime │ ├── CalendarService.ceylon │ ├── Chime.ceylon │ ├── ChimeServiceProvider.ceylon │ ├── CronBuilder.ceylon │ ├── Operator.ceylon │ ├── Scheduler.ceylon │ ├── SchedulerImpl.ceylon │ ├── SchedulerInfo.ceylon │ ├── SchedulerManager.ceylon │ ├── Sender.ceylon │ ├── State.ceylon │ ├── TimeScheduler.ceylon │ ├── TimeServices.ceylon │ ├── TimeUnit.ceylon │ ├── Timer.ceylon │ ├── TimerContainer.ceylon │ ├── TimerCreator.ceylon │ ├── TimerEvent.ceylon │ ├── TimerImpl.ceylon │ ├── TimerInfo.ceylon │ ├── UnionBuilder.ceylon │ ├── calendarBuilder.ceylon │ ├── cron │ ├── CronExpression.ceylon │ ├── DaysOrder.ceylon │ ├── calendar.ceylon │ ├── cronDefinitions.ceylon │ ├── package.ceylon │ ├── parseCron.ceylon │ ├── parseCronDaysOfWeek.ceylon │ ├── parseCronRange.ceylon │ ├── parseCronStyle.ceylon │ └── stringToInteger.ceylon │ ├── dateTimeFromJSON.ceylon │ ├── every.ceylon │ ├── extractors.ceylon │ ├── module.ceylon │ ├── package.ceylon │ ├── schedulerTopLevel.ceylon │ ├── service │ ├── ChimeServices.ceylon │ ├── Extension.ceylon │ ├── calendar │ │ ├── Annually.ceylon │ │ ├── Calendar.ceylon │ │ ├── CalendarFactory.ceylon │ │ ├── Daily.ceylon │ │ ├── Dativity.ceylon │ │ ├── DayMonth.ceylon │ │ ├── IntersectionCalendar.ceylon │ │ ├── LastDayOfWeek.ceylon │ │ ├── Monthly.ceylon │ │ ├── UnionCalendar.ceylon │ │ ├── WeekdayOfMonth.ceylon │ │ ├── Weekly.ceylon │ │ ├── extractCalendars.ceylon │ │ └── package.ceylon │ ├── message │ │ ├── DirectMessageSourceFactory.ceylon │ │ ├── MessageSource.ceylon │ │ ├── MessageSourceFactory.ceylon │ │ └── package.ceylon │ ├── package.ceylon │ ├── producer │ │ ├── EBProducerFactory.ceylon │ │ ├── EventBusProducer.ceylon │ │ ├── EventProducer.ceylon │ │ ├── ProducerFactory.ceylon │ │ └── package.ceylon │ ├── timer │ │ ├── CronFactory.ceylon │ │ ├── IntervalFactory.ceylon │ │ ├── TimeRow.ceylon │ │ ├── TimeRowCronStyle.ceylon │ │ ├── TimeRowFactory.ceylon │ │ ├── TimeRowInterval.ceylon │ │ ├── TimeRowUnion.ceylon │ │ ├── UnionFactory.ceylon │ │ ├── emptyTimeRow.ceylon │ │ └── package.ceylon │ └── timezone │ │ ├── JVMTimeZone.ceylon │ │ ├── JVMTimeZoneFactory.ceylon │ │ ├── TimeZone.ceylon │ │ ├── TimeZoneFactory.ceylon │ │ └── package.ceylon │ └── timerTopLevel.ceylon └── test └── herd └── test └── schedule └── chime ├── CronBuilder.ceylon ├── SchedulerTimer.ceylon ├── SimpleTimers.ceylon ├── module.ceylon └── package.ceylon /.ceylon/config: -------------------------------------------------------------------------------- 1 | 2 | [compiler] 3 | source=source 4 | resource=resource 5 | source=test 6 | source=examples 7 | 8 | [defaults] 9 | encoding=UTF-8 10 | -------------------------------------------------------------------------------- /.ceylon/config~: -------------------------------------------------------------------------------- 1 | 2 | [compiler] 3 | source=source 4 | resource=resource 5 | source=examples 6 | 7 | [defaults] 8 | encoding=UTF-8 9 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | /.exploded/ 14 | /modules 15 | /classes/ 16 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chime 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | com.redhat.ceylon.eclipse.ui.ceylonBuilder 15 | 16 | 17 | systemRepo 18 | 19 | 20 | 21 | 22 | 23 | 24 | com.redhat.ceylon.eclipse.ui.ceylonNature 25 | org.eclipse.jdt.core.javanature 26 | 27 | 28 | -------------------------------------------------------------------------------- /.settings/.gitignore: -------------------------------------------------------------------------------- 1 | /org.eclipse.jdt.core.prefs 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Lisi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Chime. 2 | 3 | _Chime_ is time scheduler verticle which works on [Vert.x](http://vertx.io/) event bus and provides: 4 | * scheduling with _cron-style_, _interval_, _union_ or _custom_ timers: 5 | * at a certain time of day (to the second) 6 | * on certain days of the week, month or year 7 | * with a given time interval 8 | * with nearly any combination of all of above 9 | * repeating a given number of times 10 | * repeating until a given time / date 11 | * repeating infinitely 12 | * proxying event bus with conventional interfaces 13 | * applying time zones available on _JVM_ 14 | * flexible timers management system: 15 | * grouping timers 16 | * defining a timer start or end time 17 | * pausing / resuming 18 | * fire counting 19 | * listening and sending messages via event bus with _JSON_ 20 | * _publishing_ or _sending_ timer fire event to the address of your choice 21 | 22 | _Chime_ is written in [Ceylon](https://ceylon-lang.org) and is available at 23 | [Ceylon Herd](https://herd.ceylon-lang.org/modules/herd.schedule.chime) 24 | 25 | > Runs with Ceylon 1.3.2 and Vert.x 3.4.1 26 | 27 | 28 | ## Usage and documentation. 29 | 30 | 1. Deploy _Chime_ verticle 31 | 2. Create and listen timers on _EventBus_, see details in [API docs](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.0/module-doc/api/index.html) 32 | 33 | > _Chime_ communicates over event bus with `Json` messages. 34 | Complete list of messages is available [here](../../wiki/Messages) 35 | 36 | Examples: 37 | * [with Ceylon](examples/herd/examples/schedule/chime) 38 | * [with Java and Maven](https://github.com/LisiLisenok/ChimeJavaExample) 39 | 40 | 41 | [Blog post at Vert.x website](http://vertx.io/blog/time-scheduling-with-chime/) 42 | -------------------------------------------------------------------------------- /examples/herd/examples/schedule/chime/customtimer/customTimer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | dateTime, 3 | DateTime 4 | } 5 | import herd.schedule.chime.service.timer { 6 | TimeRowFactory, 7 | TimeRow 8 | } 9 | import herd.schedule.chime.service { 10 | ChimeServices, 11 | Extension 12 | } 13 | import ceylon.json { 14 | JsonObject 15 | } 16 | import herd.schedule.chime { 17 | Chime 18 | } 19 | 20 | 21 | // Custom timer factory 22 | service(`interface Extension`) 23 | shared class CustomIntervalTimerFactory satisfies TimeRowFactory 24 | { 25 | shared static String timerType = "custom interval"; 26 | 27 | shared new () {} 28 | 29 | // Custom timer 30 | class TimeRowInterval ( 31 | "Timing delay in miliseconds, to be >= 0." shared Integer intervalMilliseconds 32 | ) satisfies TimeRow { 33 | "Current date and time." 34 | variable DateTime currentDate = dateTime(0, 1, 1); 35 | shared actual DateTime? start(DateTime current) => currentDate = current.plusMilliseconds(intervalMilliseconds); 36 | shared actual DateTime? shiftTime() => currentDate = currentDate.plusMilliseconds(intervalMilliseconds); 37 | } 38 | 39 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) { 40 | if (is Integer delay = description[Chime.key.delay]) { 41 | if (delay > 0) { 42 | return TimeRowInterval(delay * 1000); 43 | } 44 | else { 45 | return Chime.errors.codeDelayHasToBeGreaterThanZero->Chime.errors.delayHasToBeGreaterThanZero; 46 | } 47 | } 48 | return Chime.errors.codeDelayHasToBeSpecified->Chime.errors.delayHasToBeSpecified; 49 | } 50 | 51 | shared actual String type => timerType; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /examples/herd/examples/schedule/chime/customtimer/package.ceylon: -------------------------------------------------------------------------------- 1 | "Custom timer example." 2 | since( "0.1.1" ) by( "Lis" ) 3 | shared package herd.examples.schedule.chime.customtimer; 4 | -------------------------------------------------------------------------------- /examples/herd/examples/schedule/chime/customtimer/run.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core.eventbus { 2 | Message 3 | } 4 | import ceylon.json { 5 | JsonObject, 6 | JsonArray 7 | } 8 | import herd.schedule.chime { 9 | Chime 10 | } 11 | import io.vertx.ceylon.core { 12 | DeploymentOptions, 13 | vertx 14 | } 15 | 16 | 17 | shared void run() { 18 | String timerName = "scheduler:timer"; 19 | value v = vertx.vertx(); 20 | Chime c = Chime(); 21 | // deploy Chime using config with search service providers module list 22 | c.deploy ( 23 | v, 24 | DeploymentOptions ( 25 | JsonObject { 26 | // module to search custom timer 27 | Chime.configuration.services -> JsonArray{"herd.examples.schedule.chime/0.3.0"} 28 | } 29 | ), 30 | (String|Throwable res) { 31 | if (is String res) { 32 | // print installed extension 33 | v.eventBus().send ( 34 | "chime", 35 | JsonObject { 36 | Chime.key.operation -> Chime.operation.info 37 | }, 38 | (Throwable|Message msg) { 39 | if (is Message msg, exists body = msg.body()) { 40 | if (exists services = body.getArrayOrNull(Chime.configuration.services)) { 41 | for (item in services) { 42 | print(item); 43 | } 44 | } 45 | else { 46 | print("extensions are not given"); 47 | } 48 | } 49 | else { 50 | print(msg); 51 | } 52 | } 53 | ); 54 | // listen timer messages 55 | v.eventBus().consumer ( 56 | timerName, 57 | (Throwable|Message msg) { 58 | if (is Message msg, exists body = msg.body()) { 59 | if (is String event = body[Chime.key.event]) { 60 | if (event == Chime.event.complete) { 61 | print("completed"); 62 | v.close(); 63 | } 64 | else if (event == Chime.event.fire) { 65 | print("fire at ``body[Chime.key.time] else ""`` with ``body[Chime.key.message] else ""``"); 66 | } 67 | else { 68 | print("undefined event: ``event``"); 69 | v.close(); 70 | } 71 | } 72 | else { 73 | print("no event in ``msg``"); 74 | v.close(); 75 | } 76 | } 77 | else { 78 | print(msg); 79 | v.close(); 80 | } 81 | } 82 | ); 83 | // create custom timer 84 | v.eventBus().send ( 85 | "chime", 86 | JsonObject { 87 | Chime.key.operation -> Chime.operation.create, 88 | Chime.key.name -> timerName, 89 | Chime.key.maxCount -> 3, 90 | Chime.key.description -> JsonObject { 91 | // the same type as factory marked with 92 | Chime.key.type -> CustomIntervalTimerFactory.timerType, 93 | Chime.key.delay -> 1 94 | }, 95 | Chime.key.message -> "timer message" 96 | } 97 | ); 98 | } 99 | else { 100 | print("deploying error: ``res``"); 101 | v.close(); 102 | } 103 | } 104 | ); 105 | 106 | } 107 | -------------------------------------------------------------------------------- /examples/herd/examples/schedule/chime/module.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "_Chime_ examples." 3 | since("0.1.1") by("Lis") 4 | native("jvm") 5 | module herd.examples.schedule.chime "0.3.0" { 6 | shared import io.vertx.ceylon.core "3.4.2"; 7 | shared import herd.schedule.chime "0.3.0"; 8 | } 9 | -------------------------------------------------------------------------------- /examples/herd/examples/schedule/chime/package.ceylon: -------------------------------------------------------------------------------- 1 | "Simple example of cron timer." 2 | since( "0.1.1" ) by( "Lis" ) 3 | shared package herd.examples.schedule.chime; 4 | -------------------------------------------------------------------------------- /examples/herd/examples/schedule/chime/run.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core.eventbus { 2 | 3 | EventBus, 4 | Message 5 | } 6 | import ceylon.json { 7 | 8 | JsonObject 9 | } 10 | import io.vertx.ceylon.core { 11 | 12 | Vertx, 13 | vertx 14 | } 15 | import herd.schedule.chime { 16 | Chime 17 | } 18 | 19 | 20 | "Runs the module `herd.examples.schedule.chime`." 21 | shared void run() { 22 | value v = vertx.vertx(); 23 | Chime c = Chime(); 24 | c.deploy ( 25 | v, null, 26 | (String|Throwable res) { 27 | if (is String res) { 28 | value scheduler = Scheduler(v); 29 | scheduler.initialize(); 30 | } 31 | else { 32 | print("deploying error: ``res``"); 33 | v.close(); 34 | } 35 | } 36 | ); 37 | } 38 | 39 | 40 | "Performs scheduler run. Creates cron-style timer and listens it." 41 | class Scheduler(Vertx v, String address = Chime.configuration.defaultAddress) 42 | { 43 | EventBus eventBus = v.eventBus(); 44 | 45 | 46 | "Initializes testing - creates schedule manager and timer." 47 | shared void initialize() { 48 | eventBus.send ( 49 | address, 50 | JsonObject { 51 | Chime.key.operation -> Chime.operation.create, 52 | Chime.key.name -> "scheduler", 53 | Chime.key.state -> Chime.state.running, 54 | Chime.key.timeZone -> "Europe/Paris" 55 | }, 56 | (Throwable | Message msg) { 57 | if (is Message msg) { 58 | schedulerCreated(msg); 59 | } 60 | else { 61 | print("error when creating scheduler: ``msg``"); 62 | v.close(); 63 | } 64 | } 65 | ); 66 | } 67 | 68 | 69 | void printMessage(Throwable|Message msg) { 70 | if (is Message msg) { 71 | if (exists body = msg.body()) { 72 | print(body); 73 | if (is String event = body[Chime.key.event], event == Chime.event.complete) { 74 | v.close(); 75 | } 76 | } 77 | else { 78 | print("no body in the message"); 79 | v.close(); 80 | } 81 | } 82 | else { 83 | print("error: ``msg``"); 84 | v.close(); 85 | } 86 | } 87 | 88 | 89 | void schedulerCreated(Message msg) { 90 | 91 | eventBus.consumer("scheduler:timer", printMessage); 92 | 93 | eventBus.send ( 94 | address, 95 | JsonObject { 96 | Chime.key.operation -> Chime.operation.create, 97 | Chime.key.name -> "scheduler:timer", 98 | Chime.key.maxCount -> 5, 99 | //Chime.key.timeZone -> "Europe/Paris", 100 | Chime.key.description -> JsonObject { 101 | Chime.key.type -> Chime.type.cron, 102 | Chime.date.seconds -> "20/15", 103 | Chime.date.minutes -> "*", 104 | Chime.date.hours -> "0-23", 105 | Chime.date.daysOfMonth -> "1-31", 106 | Chime.date.months -> "*", 107 | Chime.date.daysOfWeek -> "*", 108 | Chime.date.years -> "2015-2019" 109 | } 110 | }, 111 | printMessage 112 | ); 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /scheduling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Time scheduling with Chime 3 | template: post.html 4 | date: 2017-05-09 5 | author: LisiLisenok 6 | draft: true 7 | --- 8 | 9 | ## Time scheduling. 10 | 11 | Vert.x executes periodic and delayed actions with 12 | [one-shot and periodic timers](http://vertx.io/docs/vertx-core/java/#_executing_periodic_and_delayed_actions). 13 | This is the base for time scheduling and reach feature extension must be rather interesting. 14 | Be notified at certain date / time, take into account holidays, 15 | repeat notifications until a given date, apply time zone, 16 | take into account daylight saving time etc. 17 | There are a lot of useful features time scheduler may introduce to the Vert.x stack. 18 | 19 | 20 | ## Chime. 21 | 22 | [Chime](https://github.com/LisiLisenok/Chime) is time scheduler verticle which works on _Vert.x_ event bus and provides: 23 | * scheduling with _cron-style_, _interval_ or _union_ timers: 24 | * at a certain time of day (to the second); 25 | * on certain days of the week, month or year; 26 | * with a given time interval; 27 | * with nearly any combination of all of above; 28 | * repeating a given number of times; 29 | * repeating until a given time / date; 30 | * repeating infinitely 31 | * proxying event bus with conventional interfaces 32 | * applying time zones available on _JVM_ 33 | * flexible timers management system: 34 | * grouping timers; 35 | * defining a timer start or end times 36 | * pausing / resuming; 37 | * fire counting; 38 | * listening and sending messages via event bus with _JSON_; 39 | * _publishing_ or _sending_ timer fire event to the address of your choice. 40 | 41 | [INFO _Chime_ is written in [Ceylon](https://ceylon-lang.org) and is available at 42 | [Ceylon Herd](https://herd.ceylon-lang.org/modules/herd.schedule.chime).] 43 | 44 | 45 | ## Running. 46 | 47 | ### Ceylon users. 48 | 49 | Deploy _Chime_ using `Verticle.deployVerticle` method. 50 | 51 | ```Ceylon 52 | import io.vertx.ceylon.core {vertx} 53 | import herd.schedule.chime {Chime} 54 | Chime().deploy(vertx.vertx()); 55 | ``` 56 | 57 | Or with `vertx.deployVerticle(\"ceylon:herd.schedule.chime/0.2.1\");` 58 | but ensure that Ceylon verticle factory is available at class path. 59 | 60 | ### Java users. 61 | 62 | 1. Ensure that Ceylon verticle factory is available at class path. 63 | 2. Put Ceylon versions to consistency. For instance, Vert.x 3.4.1 depends on Ceylon 1.3.0 64 | while Chime 0.2.1 depends on Ceylon 1.3.2. 65 | 3. [Deploy verticle](http://vertx.io/docs/vertx-core/java/#_deploying_verticles_programmatically), like: 66 | ```Java 67 | vertx.deployVerticle("ceylon:herd.schedule.chime/0.2.1") 68 | ``` 69 | 70 | [INFO example with Maven is available at [Github](https://github.com/LisiLisenok/ChimeJavaExample).] 71 | 72 | 73 | ## Schedulers. 74 | 75 | Well, _Chime_ verticle is deployed. Let's see its structure. 76 | In order to provide flexible and broad ways to manage timing two level architecture is adopted. 77 | It consists of schedulers and timers. Timer is a unit which fires at a given time. 78 | While scheduler is a set or group of timers and provides following: 79 | * creating and deleting timers; 80 | * pausing / resuming all timers working within the scheduler; 81 | * info on the running timers; 82 | * default time zone; 83 | * listening event bus at the given scheduler address for the requests to. 84 | 85 | Any timer operates within some scheduler. And one or several schedulers have to be created before starting scheduling. 86 | When _Chime_ verticle is deployed it starts listen event bus at **chime** address (can be configured). 87 | In order to create scheduler send to this address a JSON message. 88 | 89 | ```json 90 | { 91 | "operation": "create", 92 | "name": "scheduler name" 93 | } 94 | ``` 95 | 96 | 97 | Once scheduler is created it starts listen event bus at **scheduler name** address. 98 | Sending messages to **chime** address or to **scheduler name** address are rather equivalent, 99 | excepting that chime address provides services for every scheduler, while scheduler address 100 | provides services for this particular scheduler only. 101 | The request sent to the _Chime_ has to contain **operation** and **name** keys. 102 | Name key provides scheduler or timer name. While operation key shows an action _Chime_ has to perform. 103 | There are only four possible operations: 104 | * create - create new scheduler or timer; 105 | * delete - delete scheduler or timer; 106 | * info - request info on _Chime_ or on a particular scheduler or timer; 107 | * state - set or get scheduler or timer state (running, paused or completed). 108 | 109 | 110 | ## Timers. 111 | 112 | Now we have scheduler created and timers can be run within. There are two ways to access a given timer: 113 | 1. Sending message to **chime** address with 'name' field set to **scheduler name:timer name**. 114 | 2. Sending message to **scheduler name** address with 'name' field set to either **timer name** or **scheduler name:timer name**. 115 | 116 | [Timer request](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.1/module-doc/api/index.html#timer-request) is rather complicated and contains a lot of details. In this blog post only basic features are considered: 117 | 118 | ```json 119 | { 120 | "operation": "create", 121 | "name": "scheduler name:timer name", 122 | "description": {} 123 | }; 124 | ``` 125 | 126 | This is rather similar to request sent to create a scheduler. 127 | The difference is only **description** field is added. 128 | This description is an JSON object which identifies particular timer type and its details. 129 | The other fields not shown here are optional and includes: 130 | * initial timer state (paused or running); 131 | * start or end date-time; 132 | * number of repeating times; 133 | * is timer message to be published or sent; 134 | * timer fire message and delivery options; 135 | * time zone. 136 | 137 | 138 | ## Timer descriptions. 139 | 140 | Currently, three types of timers are supported: 141 | 142 | * __Interval timer__ which fires after each given time period (minimum 1 second): 143 | ```json 144 | { 145 | "type": "interval", 146 | "delay": "timer delay in seconds, Integer" 147 | }; 148 | ``` 149 | 150 | * __Cron style timer__ which is defined with cron-style: 151 | ```json 152 | { 153 | "type": "cron", 154 | "seconds": "seconds in cron style, String", 155 | "minutes": "minutes in cron style, String", 156 | "hours": "hours in cron style, String", 157 | "days of month": "days of month in cron style, String", 158 | "months": "months in cron style, String", 159 | "days of week": "days of week in cron style, String, optional", 160 | "years": "years in cron style, String, optional" 161 | }; 162 | ``` 163 | Cron timer is rather powerful and flexible. Investigate [specification](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.1/module-doc/api/index.html#cron-expression) for the complete list of features. 164 | 165 | * __Union timer__ which combines a number of timers into a one: 166 | ```json 167 | { 168 | "type": "union", 169 | "timers": ["list of the timer descriptions"] 170 | }; 171 | ``` 172 | Union timer may be useful to fire at a list of specific dates / times. 173 | 174 | 175 | ## Timer events. 176 | 177 | Once timer is started it sends or publishes messages to **scheduler name:timer name** address in JSON format. 178 | Two types of events are sent: 179 | * fire event which occurs when time reaches next timer value: 180 | ```json 181 | { 182 | "name": "scheduler name:timer name, String", 183 | "event": "fire", 184 | "count": "total number of fire times, Integer", 185 | "time": "ISO formated time / date, String", 186 | "seconds": "number of seconds since last minute, Integer", 187 | "minutes": "number of minutes since last hour, Integer", 188 | "hours": "hour of day, Integer", 189 | "day of month": "day of month, Integer", 190 | "month": "month, Integer", 191 | "year": "year, Integer", 192 | "time zone": "time zone the timer works in, String" 193 | }; 194 | ``` 195 | * complete event which occurs when timer is exhausted by some criteria given at timer create request: 196 | ```json 197 | { 198 | "name": "scheduler name:timer name, String", 199 | "event": "complete", 200 | "count": "total number of fire times, Integer" 201 | }; 202 | ``` 203 | 204 | Basically, now we know everything to be happy with _Chime_: schedulers and requests to them, timers and timer events. 205 | Will see some examples in the next section. 206 | 207 | 208 | ## Examples. 209 | 210 | ### Ceylon example. 211 | 212 | Let's consider a timer which has to fire every month at 16-30 last Sunday. 213 | 214 | ```Ceylon 215 | // listen the timer events 216 | eventBus.consumer ( 217 | "my scheduler:my timer", 218 | (Throwable|Message msg) { 219 | if (is Message msg) { print(msg.body()); } 220 | else { print(msg); } 221 | } 222 | ); 223 | // create scheduler and timer 224 | eventBus.send ( 225 | "chime", 226 | JsonObject { 227 | "operation" -> "create", 228 | "name" -> "my scheduler:my timer", 229 | "description" -> JsonObject { 230 | "type" -> "cron", 231 | "seconds" -> "0", 232 | "minutes" -> "30", 233 | "hours" -> "16", 234 | "days of month" -> "*", 235 | "months" -> "*", 236 | "days of week" -> "SundayL" 237 | } 238 | } 239 | ); 240 | 241 | ``` 242 | 243 | [INFO '*' means any, 'SundayL' means last Sunday.] 244 | 245 | [NOTE If 'create' request is sent to Chime address with name set to 'scheduler name:timer name' and corresponding scheduler hasn't been created before then Chime creates both new scheduler and new timer.] 246 | 247 | 248 | ### Java example. 249 | 250 | Let's consider a timer which has to fire every Monday at 8-30 and every Friday at 17-30. 251 | 252 | ```Java 253 | // listen the timer events 254 | MessageConsumer consumer = eventBus.consumer("my scheduler:my timer"); 255 | consumer.handler ( 256 | message -> { 257 | System.out.println(message.body()); 258 | } 259 | ); 260 | // description of timers 261 | JsonObject mondayTimer = (new JsonObject()).put("type", "cron") 262 | .put("seconds", "0").put("minutes", "30").put("hours", "8") 263 | .put("days of month", "*").put("months", "*") 264 | .put("days of week", "Monday"); 265 | JsonObject fridayTimer = (new JsonObject()).put("type", "cron") 266 | .put("seconds", "0").put("minutes", "30").put("hours", "17") 267 | .put("days of month", "*").put("months", "*") 268 | .put("days of week", "Friday"); 269 | // union timer - combines mondayTimer and fridayTimer 270 | JsonArray combination = (new JsonArray()).add(mondayTimer) 271 | .add(fridayTimer); 272 | JsonObject timer = (new JsonObject()).put("type", "union") 273 | .put("timers", combination); 274 | // create scheduler and timer 275 | eventBus.send ( 276 | "chime", 277 | (new JsonObject()).put("operation", "create") 278 | .put("name", "my scheduler:my timer") 279 | .put("description", timer) 280 | ); 281 | ``` 282 | 283 | [IMPORTANT Ensure that Ceylon verticle factory with right version is available at class path.] 284 | 285 | 286 | ## At the end. 287 | 288 | `herd.schedule.chime` module provides some features not mentioned here: 289 | * convenient builders useful to fill in JSON description of various timers; 290 | * proxying event bus with conventional interfaces; 291 | * reading JSON timer event into an object; 292 | * attaching JSON message to the timer fire event; 293 | * managing time zones. 294 | 295 | There are also some ideas for the future: 296 | * custom or user-defined timers; 297 | * limiting the timer fire time / date with calendar; 298 | * extracting timer fire message from external source. 299 | 300 | ----------------------------- 301 | 302 | This is very quick introduction to the _Chime_ and if you are interested in you may read 303 | more in [Chime documentation](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.1/module-doc/api/index.html) or even [contribute](https://github.com/LisiLisenok/Chime) to. 304 | 305 | Thank's for the reading and enjoy with coding! 306 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/CalendarService.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import herd.schedule.chime.service.calendar { 5 | Calendar 6 | } 7 | 8 | 9 | "Calendar + ignorance flage." 10 | since("0.3.0") by("Lis") 11 | interface CalendarService satisfies Calendar { 12 | "Calendar ignorance flag." shared formal Boolean calendarIgnorance; 13 | } 14 | 15 | 16 | "Implementation of `CalendarService`." 17 | since("0.3.0") by("Lis") 18 | class CalendarServiceImpl ( 19 | shared actual Boolean calendarIgnorance, 20 | Calendar calendar 21 | ) 22 | satisfies CalendarService 23 | { 24 | shared actual Boolean inside(DateTime date) => calendar.inside(date); 25 | 26 | shared actual DateTime nextOutside(DateTime date) => calendar.nextOutside(date); 27 | } 28 | 29 | 30 | "[[CalendarService]] which restricts nothing." 31 | since("0.3.0") by("Lis") 32 | object emptyCalendar satisfies CalendarService { 33 | shared actual Boolean inside(DateTime date) => false; 34 | shared actual DateTime nextOutside(DateTime date) => date.plusSeconds(1); 35 | shared actual Boolean calendarIgnorance => true; 36 | } 37 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/ChimeServiceProvider.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | ChimeServices, 3 | Extension 4 | } 5 | import herd.schedule.chime.service.timezone { 6 | TimeZone, 7 | JVMTimeZoneFactory 8 | } 9 | import ceylon.collection { 10 | HashMap, 11 | ArrayList 12 | } 13 | import ceylon.json { 14 | JsonObject, 15 | JsonArray 16 | } 17 | import ceylon.language.meta { 18 | modules, 19 | type 20 | } 21 | import ceylon.language.meta.declaration { 22 | Module 23 | } 24 | import ceylon.language.meta.model { 25 | ClassOrInterface, 26 | Type 27 | } 28 | import io.vertx.ceylon.core { 29 | Vertx, 30 | Future, 31 | future 32 | } 33 | 34 | 35 | "Provides Chime services." 36 | since("0.3.0") by("Lis") 37 | class ChimeServiceProvider satisfies ChimeServices 38 | { 39 | 40 | static String[2]? moduleNameAndVersion(String fullName) { 41 | value r = fullName.split('/'.equals); 42 | if (r.size == 2) { 43 | return [r.first.normalized, r.last.normalized]; 44 | } 45 | else { 46 | return null; 47 | } 48 | } 49 | 50 | 51 | "Collects service providers from the given module." 52 | static void collectFromModule ( 53 | "Module to extract service provider from." Module m, 54 | "Interface represented service." ClassOrInterface> providerType, 55 | "Map to collect providers. `key` is given with [[Extension.type]]." ArrayList> extensions 56 | ) { 57 | value services = m.findServiceProviders>(providerType); 58 | for (serv in services) { 59 | extensions.add(serv); 60 | } 61 | } 62 | 63 | "Collects time row service providers." 64 | static {Extension*} collectServices ( 65 | "module name + version" {String*} moduleNames, 66 | "Service type." ClassOrInterface> providerType 67 | ) { 68 | ArrayList> extensions = ArrayList>(); 69 | for (moduleName in moduleNames) { 70 | if (exists splitName = moduleNameAndVersion(moduleName), 71 | exists m = modules.find(splitName[0], splitName[1]) 72 | ) { 73 | collectFromModule(m, providerType, extensions); 74 | } 75 | else { 76 | // TODO: log the module hasn't been found 77 | } 78 | } 79 | if (extensions.empty) { 80 | // If nothing has been added - add extensions from this modue only, 81 | // otherwise extensions from this module have to be already added 82 | collectFromModule(`module`, providerType, extensions); 83 | } 84 | return extensions; 85 | } 86 | 87 | 88 | "Factories to create extension." 89 | HashMap, HashMap>> providers 90 | = HashMap, HashMap>>(); 91 | 92 | shared actual Vertx vertx; 93 | shared actual String address; 94 | 95 | 96 | "Instantiates new Chime service provider." 97 | shared new ( 98 | "Vertx the Chime is run within." Vertx vertx, 99 | "Event bus address the _Chime_ listens to." String address 100 | ) { 101 | this.vertx = vertx; 102 | this.address = address; 103 | } 104 | 105 | "Initializes the given list of extensions." 106 | Future initializeExtensions ( 107 | "Configuration the _Chime_ is started with." JsonObject config, 108 | "List of the providers to be initialized." {Extension*} uninitialized, 109 | "Map to add successfully initialized providers." 110 | HashMap, HashMap>> toAddProviders 111 | ) { 112 | Integer total = uninitialized.size; 113 | if (total > 0) { 114 | variable Integer initialized = 0; 115 | Future ret = future.future(); 116 | 117 | value added = (Extension|Throwable provider) { 118 | if (is Extension provider) { 119 | if (exists m = toAddProviders[provider.parameter]) { 120 | m.put(provider.type, provider); 121 | } 122 | else { 123 | value m = HashMap>(); 124 | m.put(provider.type, provider); 125 | toAddProviders.put(provider.parameter, m); 126 | } 127 | } 128 | else { 129 | // TODO: log the extension hasn't been initialized 130 | } 131 | initialized ++; 132 | if (initialized >= total) { 133 | ret.complete(); 134 | } 135 | }; 136 | 137 | for (provider in uninitialized) { 138 | provider.initialize(vertx, config, added); 139 | } 140 | return ret; 141 | } 142 | else { 143 | return future.succeededFuture(); 144 | } 145 | } 146 | 147 | "Initializes all external service providers." 148 | shared void initialize ( 149 | "Configuration the _Chime_ is started with." JsonObject config, 150 | "Completion handler." Anything(Anything) complete 151 | ) { 152 | {String*} services = if (is JsonArray servicesArray = config.get(Chime.configuration.services)) 153 | then servicesArray.narrow() else {}; 154 | initializeExtensions(config, collectServices(services, `Extension`), providers).setHandler(complete); 155 | } 156 | 157 | "Returns info on the extensions." 158 | shared JsonArray extensionsInfo() { 159 | return JsonArray { 160 | for (m in providers) 161 | for (item in m.item.items) 162 | JsonObject { 163 | Chime.key.service -> m.key.string, 164 | item.type -> type(item).declaration.qualifiedName 165 | } 166 | }; 167 | } 168 | 169 | shared actual Service|String> createService ( 170 | String providerType, JsonObject options 171 | ) { 172 | if (is Extension service = providers[`Service`]?.get(providerType)) { 173 | return service.create(this, options); 174 | } 175 | else { 176 | return Chime.errors.codeUnsupportedServiceProviderType->Chime.errors.unsupportedServiceProviderType; 177 | } 178 | } 179 | 180 | shared actual TimeZone localTimeZone => JVMTimeZoneFactory.localTimeZone; 181 | 182 | } 183 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/Operator.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | 3 | JsonObject 4 | } 5 | import io.vertx.ceylon.core.eventbus { 6 | 7 | Message, 8 | EventBus, 9 | MessageConsumer 10 | } 11 | import java.util { 12 | UUID 13 | } 14 | 15 | 16 | "Provides basic operations with `JsonObject` message." 17 | see(`class SchedulerManager`, `class TimeScheduler`) 18 | since("0.1.0") by("Lis") 19 | abstract class Operator { 20 | 21 | "Generates unique string." 22 | shared static String uuidString() => UUID.randomUUID().string.replace(Chime.configuration.nameSeparator, "."); 23 | 24 | "Extracts state from request, helper method." 25 | shared static State? extractState(JsonObject request) { 26 | if (is String state = request[Chime.key.state]) { 27 | return stateByName(state); 28 | } 29 | else { 30 | return null; 31 | } 32 | } 33 | 34 | 35 | "Event bus consumer." variable MessageConsumer? consumer = null; 36 | "Address this operator listens to." shared String address; 37 | "EventBus to pass messages." shared EventBus eventBus; 38 | 39 | 40 | shared new ( 41 | "Address this operator listens to." String address, 42 | "EventBus to pass messages." EventBus eventBus 43 | ) { 44 | this.address = address; 45 | this.eventBus = eventBus; 46 | } 47 | 48 | 49 | "Operators map." 50 | late Map)> operators = createOperators(); 51 | 52 | "Creates operators map." 53 | shared formal Map)> createOperators(); 54 | 55 | 56 | "Message has been received from event bus - process it!." 57 | void onMessage("Message from event bus." Message msg) { 58 | if (exists request = msg.body(), is String operation = request[Chime.key.operation]) { 59 | // depending on operation code 60 | if (exists operator = operators[operation]) { 61 | operator(msg); 62 | } 63 | else { 64 | msg.fail(Chime.errors.codeUnsupportedOperation, Chime.errors.unsupportedOperation); 65 | } 66 | } 67 | else { 68 | // response with wrong format error 69 | msg.fail(Chime.errors.codeOperationIsNotSpecified, Chime.errors.operationIsNotSpecified); 70 | } 71 | } 72 | 73 | "Connects to event bus, returns promise resolved when event listener registered." 74 | shared default void connect(Boolean local) { 75 | "Already connected." 76 | assert(!consumer exists); 77 | // setup event bus listener 78 | if (local) { 79 | consumer = eventBus.localConsumer(address, onMessage); 80 | } 81 | else { 82 | consumer = eventBus.consumer(address, onMessage); 83 | } 84 | } 85 | 86 | shared default void stop() { 87 | consumer?.unregister(); 88 | consumer = null; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/Scheduler.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import ceylon.json { 5 | 6 | JsonObject, 7 | JsonArray, 8 | ObjectValue 9 | } 10 | 11 | 12 | "Wraps event bus to provide exchanging messages with previously created scheduler. 13 | The object implementing interface is returned by [[connectScheduler]]." 14 | see(`interface Timer`, `function connectScheduler`, `function createScheduler`) 15 | tagged("Proxy") 16 | since("0.2.0") by("Lis") 17 | shared interface Scheduler { 18 | 19 | "Name of the scheduler. 20 | _Chime_ listens event bus at this address for the messages to this scheduler. 21 | See details in [[module herd.schedule.chime]]." 22 | shared formal String name; 23 | 24 | "Removes this scheduler." 25 | shared formal void delete("Optional reply handler. Replied with scheduler name." Anything(Throwable|String)? reply = null); 26 | 27 | "Pauses this scheduler." 28 | see(`function resume`) 29 | shared formal void pause("Optional reply handler. Replied with scheduler state." Anything(Throwable|State)? reply = null); 30 | 31 | "Resumes this scheduler after pausing." 32 | see(`function pause`) 33 | shared formal void resume("Optional reply handler. Replied with scheduler state." Anything(Throwable|State)? reply = null); 34 | 35 | "Requests scheduler info." 36 | see(`function schedulerInfo`) 37 | shared formal void info("Info handler." Anything(Throwable|SchedulerInfo) info); 38 | 39 | "Requests info on a list of timers." 40 | see(`function schedulerInfo`) since("0.2.1") 41 | shared formal void timersInfo ( 42 | "Names of timers info is requested for." {String+} timers, 43 | "Info handler." Anything(Throwable|TimerInfo[]) info 44 | ); 45 | 46 | "Deletes a list of timers. 47 | `handler` is called with a list of actually deleted timers or with an error if occured. " 48 | see(`function delete`) since("0.2.1") 49 | shared formal void deleteTimers ( 50 | "Names of timers to be deleted." {String+} timers, 51 | "Optional delete handler." Anything(Throwable|{String*})? handler = null 52 | ); 53 | 54 | "Creates an interval timer." 55 | shared default void createIntervalTimer ( 56 | "Callback when timer created." 57 | Anything(Timer|Throwable) handler, 58 | "Interval timer delay in seconds." 59 | Integer delay, 60 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**. 61 | By default unique timer name is generate." 62 | String? timerName = null, 63 | "`True` if timer is paused at initial and `false` if running." 64 | Boolean paused = false, 65 | "Maximum number of fires or null if unlimited." 66 | Integer? maxCount = null, 67 | "Timer start date." 68 | DateTime? startDate = null, 69 | "Timer end date." 70 | DateTime? endDate = null, 71 | "Time zone, default is machine local." 72 | String? timeZone = null, 73 | "Optional time zone provider, default is \"jvm\"." 74 | String? timeZoneProvider = null, 75 | "Message to be passed to message source in order to extract final message." 76 | ObjectValue? message = null, 77 | "Optional message source type, default is \"direct\" which attaches `message` as is." 78 | String? messageSource = null, 79 | "Optional configuration passed to message source factory." 80 | JsonObject? messageSourceOptions = null, 81 | "Event producer provider." 82 | String? eventProducer = null, 83 | "Optional configuration passed to event producer factory." 84 | JsonObject? eventProducerOptions = null 85 | ) => createTimer ( 86 | handler, JsonObject {Chime.key.type -> Chime.type.interval, Chime.key.delay -> delay}, 87 | timerName, paused, maxCount, startDate, endDate, timeZone, timeZoneProvider, 88 | message, messageSource, messageSourceOptions, eventProducer, eventProducerOptions 89 | ); 90 | 91 | "Creates a cron timer." 92 | shared default void createCronTimer ( 93 | "Callback when timer created." Anything(Timer|Throwable) handler, 94 | "Seconds." String seconds, 95 | "Minutes." String minutes, 96 | "Hours." String hours, 97 | "Days of month." String daysOfMonth, 98 | "Months." String months, 99 | "Optional days of week." String? daysOfWeek = null, 100 | "Optional years." String? years = null, 101 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**. 102 | By default unique timer name is generate." 103 | String? timerName = null, 104 | "`True` if timer is paused at initial and `false` if running." 105 | Boolean paused = false, 106 | "Maximum number of fires or null if unlimited." 107 | Integer? maxCount = null, 108 | "Timer start date." 109 | DateTime? startDate = null, 110 | "Timer end date." 111 | DateTime? endDate = null, 112 | "Time zone." 113 | String? timeZone = null, 114 | "Optional time zone provider, default is \"jvm\"." 115 | String? timeZoneProvider = null, 116 | "Message to be passed to message source in order to extract final message." 117 | ObjectValue? message = null, 118 | "Optional message source type, default is \"direct\" which attaches `message` as is." 119 | String? messageSource = null, 120 | "Optional configuration passed to message source factory." 121 | JsonObject? messageSourceOptions = null, 122 | "Event producer provider." 123 | String? eventProducer = null, 124 | "Optional configuration passed to event producer factory." 125 | JsonObject? eventProducerOptions = null 126 | ) { 127 | JsonObject descr = JsonObject { 128 | Chime.key.type -> Chime.type.cron, 129 | Chime.date.seconds -> seconds, 130 | Chime.date.minutes -> minutes, 131 | Chime.date.hours -> hours, 132 | Chime.date.daysOfMonth -> daysOfMonth, 133 | Chime.date.months -> months 134 | }; 135 | if (exists d = daysOfWeek, !d.empty) { 136 | descr.put(Chime.date.daysOfWeek, d); 137 | } 138 | if (exists d = years, !d.empty) { 139 | descr.put(Chime.date.years, d); 140 | } 141 | createTimer ( 142 | handler, descr, timerName, paused, maxCount, startDate, endDate, timeZone, 143 | timeZoneProvider, message, messageSource, messageSourceOptions, 144 | eventProducer, eventProducerOptions 145 | ); 146 | } 147 | 148 | "Creates an union timer." 149 | since( "0.2.1" ) 150 | shared default void createUnionTimer ( 151 | "Callback when timer created." 152 | Anything(Timer|Throwable) handler, 153 | "Nonempty list of the timers to be combined into union." 154 | {JsonObject+} timers, 155 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**. 156 | By default unique timer name is generate." 157 | String? timerName = null, 158 | "`True` if timer is paused at initial and `false` if running." 159 | Boolean paused = false, 160 | "Maximum number of fires or null if unlimited." 161 | Integer? maxCount = null, 162 | "Timer start date." 163 | DateTime? startDate = null, 164 | "Timer end date." 165 | DateTime? endDate = null, 166 | "Time zone." 167 | String? timeZone = null, 168 | "Optional time zone provider, default is \"jvm\"." 169 | String? timeZoneProvider = null, 170 | "Message to be passed to message source in order to extract final message." 171 | ObjectValue? message = null, 172 | "Optional message source type, default is \"direct\" which attaches `message` as is." 173 | String? messageSource = null, 174 | "Optional configuration passed to message source factory." 175 | JsonObject? messageSourceOptions = null, 176 | "Event producer provider." 177 | String? eventProducer = null, 178 | "Optional configuration passed to event producer factory." 179 | JsonObject? eventProducerOptions = null 180 | ) => 181 | createTimer ( 182 | handler, JsonObject {Chime.key.type -> Chime.type.union, Chime.key.timers -> JsonArray(timers)}, 183 | timerName, paused, maxCount, startDate, endDate, timeZone, timeZoneProvider, 184 | message, messageSource, messageSourceOptions, eventProducer, eventProducerOptions 185 | ); 186 | 187 | 188 | "Creates a timer with the given description." 189 | since( "0.2.1" ) 190 | shared formal void createTimer ( 191 | "Callback when timer created." Anything(Timer|Throwable) handler, 192 | "JSON timer description." JsonObject description, 193 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**. 194 | By default unique timer name is generate." 195 | String? timerName = null, 196 | "`True` if timer is paused at initial and `false` if running." 197 | Boolean paused = false, 198 | "Maximum number of fires or null if unlimited." 199 | Integer? maxCount = null, 200 | "Timer start date." 201 | DateTime? startDate = null, 202 | "Timer end date." 203 | DateTime? endDate = null, 204 | "Opyional time zone, default is scheduler or local." 205 | String? timeZone = null, 206 | "Optional time zone provider, default is scheduler or \"jvm\"." 207 | String? timeZoneProvider = null, 208 | "Message to be passed to message source in order to extract final message." 209 | ObjectValue? message = null, 210 | "Optional message source type, default is scheduler or \"direct\" which attaches `message` as is." 211 | String? messageSource = null, 212 | "Optional configuration passed to message source factory." 213 | JsonObject? messageSourceOptions = null, 214 | "Event producer provider." 215 | String? eventProducer = null, 216 | "Optional configuration passed to event producer factory." 217 | JsonObject? eventProducerOptions = null 218 | ); 219 | 220 | } 221 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/SchedulerImpl.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import io.vertx.ceylon.core.eventbus { 5 | EventBus, 6 | Message 7 | } 8 | import ceylon.json { 9 | JsonObject, 10 | JsonArray, 11 | ObjectValue 12 | } 13 | 14 | 15 | "Internal implementation of [[Scheduler]]." 16 | see(`function connectScheduler`, `function createScheduler`) 17 | since("0.2.0") by("Lis") 18 | class SchedulerImpl 19 | extends Sender satisfies Scheduler 20 | { 21 | 22 | shared static void createSchedulerImpl(Anything(Throwable|Scheduler) handler, EventBus eventBus, Integer? sendTimeout) 23 | (Throwable|Message msg) 24 | { 25 | if (is Message msg) { 26 | "Reply from scheduler request has not to be null." 27 | assert (exists ret = msg.body()); 28 | handler(SchedulerImpl(ret.getString(Chime.key.name), eventBus, sendTimeout)); 29 | } 30 | else { 31 | handler(msg); 32 | } 33 | } 34 | 35 | shared static void replyWithInfo(Anything(Throwable|SchedulerInfo[]) handler) 36 | (Throwable|Message msg) 37 | { 38 | if (is Message msg) { 39 | "Reply from scheduler request has not to be null." 40 | assert (exists ret = msg.body()); 41 | value sch = ret.getArray(Chime.key.schedulers); 42 | handler([for (item in sch.narrow()) SchedulerInfo.fromJSON(item)]); 43 | } 44 | else { 45 | handler( msg ); 46 | } 47 | } 48 | 49 | shared static void replyWithList(Anything(Throwable|{String*}) handler, String key) 50 | (Throwable|Message msg) 51 | { 52 | if (is Message msg) { 53 | "Reply from scheduler request has not to be null." 54 | assert (exists ret = msg.body()); 55 | handler ( 56 | ret.getArray(Chime.key.schedulers).narrow() 57 | .chain(ret.getArray(Chime.key.timers).narrow()) 58 | ); 59 | } 60 | else { 61 | handler(msg); 62 | } 63 | } 64 | 65 | shared static void replyWithState(Anything(Throwable|State) handler) 66 | (Throwable|Message msg) 67 | { 68 | if (is Message msg) { 69 | "Reply from scheduler request has not to be null." 70 | assert (exists ret = msg.body()); 71 | "Timer info replied from scheduler has to contain state field." 72 | assert (exists state = stateByName(ret.getString(Chime.key.state))); 73 | handler(state); 74 | } 75 | else { 76 | handler(msg); 77 | } 78 | } 79 | 80 | shared static void replyWithName(Anything(Throwable|String) handler) 81 | (Throwable|Message msg) 82 | { 83 | if (is Message msg) { 84 | "Reply from scheduler request has not to be null." 85 | assert (exists ret = msg.body()); 86 | handler(ret.getString(Chime.key.name)); 87 | } 88 | else { 89 | handler(msg); 90 | } 91 | } 92 | 93 | shared static void replyWithTimer ( 94 | String? schedulerName, EventBus eventBus, Integer? sendTimeout, Anything(Throwable|TimerImpl) handler 95 | ) (Throwable | Message msg) 96 | { 97 | if (is Throwable msg) { 98 | handler(msg); 99 | } 100 | else { 101 | "Timer create request has to respond with body." 102 | assert (exists rep = msg.body()); 103 | String name = rep.getString(Chime.key.name); 104 | "Timer full name has to contain scheduler and timer names." 105 | assert (exists inc = name.firstOccurrence(Chime.configuration.nameSeparatorChar)); 106 | handler(TimerImpl(name, name.spanTo(inc - 1), eventBus, sendTimeout)); 107 | } 108 | } 109 | 110 | 111 | variable Boolean alive = true; 112 | shared actual String name; 113 | EventBus eventBus; 114 | "Timeout to send message with." Integer? sendTimeout; 115 | 116 | shared new (String name, EventBus eventBus, "Timeout to send message with." Integer? sendTimeout) 117 | extends Sender(name, eventBus, sendTimeout) 118 | { 119 | this.name = name; 120 | this.eventBus = eventBus; 121 | this.sendTimeout = sendTimeout; 122 | } 123 | 124 | shared actual void createTimer ( 125 | Anything(Timer|Throwable) handler, JsonObject description, String? timerName, 126 | Boolean paused, Integer? maxCount, DateTime? startDate, 127 | DateTime? endDate, String? timeZone, String? timeZoneProvider, 128 | ObjectValue? message, String? messageSource, JsonObject? messageSourceOptions, 129 | String? eventProducer, JsonObject? eventProducerOptions 130 | 131 | ) { 132 | JsonObject timer = JsonObject { 133 | Chime.key.operation -> Chime.operation.create 134 | }; 135 | if (exists timerName) { 136 | timer.put( Chime.key.name, timerName ); 137 | } 138 | if (paused) { 139 | timer.put(Chime.key.state, Chime.state.paused); 140 | } 141 | if (exists maxCount) { 142 | timer.put(Chime.key.maxCount, maxCount); 143 | } 144 | if (exists startDate) { 145 | timer.put ( 146 | Chime.key.startTime, 147 | JsonObject { 148 | Chime.date.seconds -> startDate.seconds, 149 | Chime.date.minutes -> startDate.minutes, 150 | Chime.date.hours -> startDate.hours, 151 | Chime.date.dayOfMonth -> startDate.day, 152 | Chime.date.month -> startDate.month.integer, 153 | Chime.date.year -> startDate.year 154 | } 155 | ); 156 | } 157 | if (exists endDate) { 158 | timer.put ( 159 | Chime.key.endTime, 160 | JsonObject { 161 | Chime.date.seconds -> endDate.seconds, 162 | Chime.date.minutes -> endDate.minutes, 163 | Chime.date.hours -> endDate.hours, 164 | Chime.date.dayOfMonth -> endDate.day, 165 | Chime.date.month -> endDate.month.integer, 166 | Chime.date.year -> endDate.year 167 | } 168 | ); 169 | } 170 | if (exists timeZone) { 171 | timer.put(Chime.key.timeZone, timeZone); 172 | } 173 | if (exists timeZoneProvider) { 174 | timer.put(Chime.key.timeZoneProvider, timeZoneProvider); 175 | } 176 | if (exists message) { 177 | timer.put(Chime.key.message, message); 178 | } 179 | if (exists messageSource) { 180 | timer.put( Chime.key.messageSource, messageSource ); 181 | } 182 | if ( exists messageSourceOptions ) { 183 | timer.put( Chime.key.messageSourceOptions, messageSourceOptions ); 184 | } 185 | if ( exists eventProducer ) { 186 | timer.put(Chime.key.eventProducer, eventProducer); 187 | } 188 | if (exists eventProducerOptions) { 189 | timer.put(Chime.key.eventProducerOptions, eventProducerOptions); 190 | } 191 | timer.put(Chime.key.description, description); 192 | 193 | sendRepliedRequest(timer, replyWithTimer(name, eventBus, sendTimeout, handler)); 194 | } 195 | 196 | 197 | shared actual void delete(Anything(Throwable|String)? reply) { 198 | if (alive) { 199 | alive = false; 200 | if (exists reply) { 201 | sendRepliedRequest ( 202 | JsonObject { 203 | Chime.key.operation -> Chime.operation.delete, 204 | Chime.key.name -> name 205 | }, 206 | replyWithName(reply) 207 | ); 208 | } 209 | else { 210 | sendRequest ( 211 | JsonObject { 212 | Chime.key.operation -> Chime.operation.delete, 213 | Chime.key.name -> name 214 | } 215 | ); 216 | } 217 | } 218 | } 219 | 220 | shared actual void pause( Anything(Throwable|State)? reply ) { 221 | if ( alive ) { 222 | if ( exists reply ) { 223 | sendRepliedRequest ( 224 | JsonObject { 225 | Chime.key.operation -> Chime.operation.state, 226 | Chime.key.name -> name, 227 | Chime.key.state -> Chime.state.paused 228 | }, 229 | replyWithState(reply) 230 | ); 231 | } 232 | else { 233 | sendRequest ( 234 | JsonObject { 235 | Chime.key.operation -> Chime.operation.state, 236 | Chime.key.name -> name, 237 | Chime.key.state -> Chime.state.paused 238 | } 239 | ); 240 | } 241 | } 242 | } 243 | 244 | shared actual void resume(Anything(Throwable|State)? reply) { 245 | if (alive) { 246 | if (exists reply) { 247 | sendRepliedRequest ( 248 | JsonObject { 249 | Chime.key.operation -> Chime.operation.state, 250 | Chime.key.name -> name, 251 | Chime.key.state -> Chime.state.running 252 | }, 253 | replyWithState(reply) 254 | ); 255 | } 256 | else { 257 | sendRequest ( 258 | JsonObject { 259 | Chime.key.operation -> Chime.operation.state, 260 | Chime.key.name -> name, 261 | Chime.key.state -> Chime.state.running 262 | } 263 | ); 264 | } 265 | } 266 | } 267 | 268 | shared actual void info(Anything(Throwable|SchedulerInfo) info) { 269 | sendRepliedRequest ( 270 | JsonObject { 271 | Chime.key.operation -> Chime.operation.info 272 | }, 273 | (Throwable|Message msg) { 274 | if (is Message msg) { 275 | "Reply from scheduler request has not to be null." 276 | assert(exists ret = msg.body()); 277 | info(SchedulerInfo.fromJSON(ret)); 278 | } 279 | else { 280 | info(msg); 281 | } 282 | } 283 | ); 284 | } 285 | 286 | shared actual void deleteTimers({String+} timers, Anything(Throwable|{String*})? handler) { 287 | if (exists handler) { 288 | sendRepliedRequest ( 289 | JsonObject { 290 | Chime.key.operation -> Chime.operation.delete, 291 | Chime.key.name -> JsonArray(timers) 292 | }, 293 | replyWithList(handler, Chime.key.timers) 294 | ); 295 | } 296 | else { 297 | sendRequest ( 298 | JsonObject { 299 | Chime.key.operation -> Chime.operation.delete, 300 | Chime.key.name -> JsonArray(timers) 301 | } 302 | ); 303 | } 304 | } 305 | 306 | shared actual void timersInfo({String+} timers, Anything(Throwable|TimerInfo[]) info) { 307 | sendRepliedRequest ( 308 | JsonObject { 309 | Chime.key.operation -> Chime.operation.info, 310 | Chime.key.name -> JsonArray(timers) 311 | }, 312 | TimerImpl.replyWithInfo(info) 313 | ); 314 | } 315 | 316 | shared actual String string => "Scheduler ``name``"; 317 | 318 | } 319 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/SchedulerInfo.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | JsonObject 3 | } 4 | 5 | 6 | "Info on the scheduler." 7 | see(`interface Scheduler`, `class TimerInfo`, `function schedulerInfo`) 8 | tagged("Info") 9 | since("0.2.0") by("Lis") 10 | shared final class SchedulerInfo { 11 | 12 | "Scheduler name." shared String name; 13 | "Scheduler state at the request moment." shared State state; 14 | "Default time zone." shared String timeZone; 15 | "List of the timers. Actual at the request moment." shared TimerInfo[] timers; 16 | 17 | "Instantiates `SchedulerInfo` with the given parameters." 18 | shared new ( 19 | "Scheduler name." String name, 20 | "Scheduler state at the request moment." State state, 21 | "Default time zone the scheduler." String timeZone, 22 | "List of the timers. Actual at the request moment." TimerInfo[] timers 23 | ) { 24 | this.name = name; 25 | this.state = state; 26 | this.timeZone = timeZone; 27 | this.timers = timers; 28 | } 29 | 30 | "Instantiates `SchedulerInfo` from JSON description as send by _Chime_." 31 | shared new fromJSON("Scheduler info received from _Chime_." JsonObject schedulerInfo) { 32 | this.name = schedulerInfo.getString(Chime.key.name); 33 | "Scheduler info replied from _Chime_ has to contain state field." 34 | assert( exists state = stateByName(schedulerInfo.getString(Chime.key.state))); 35 | this.state = state; 36 | this.timeZone = schedulerInfo.getString(Chime.key.timeZone); 37 | if (exists arr = schedulerInfo.getArrayOrNull(Chime.key.timers)) { 38 | timers = arr.narrow().map(TimerInfo.fromJSON).sequence(); 39 | } 40 | else { 41 | timers = []; 42 | } 43 | } 44 | 45 | shared actual String string => "Info on scheduler ``name``, ``state``"; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/Sender.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core.eventbus { 2 | EventBus, 3 | DeliveryOptions, 4 | Message 5 | } 6 | import ceylon.json { 7 | JsonObject 8 | } 9 | 10 | 11 | "Base class for event bus senders." 12 | since("0.3.0") by("Lis") 13 | class Sender ( 14 | "Address to send." shared String address, 15 | "EB" shared EventBus eventBus, 16 | "Timeout to send message with." shared Integer? sendTimeout 17 | ) { 18 | 19 | DeliveryOptions? options = if (exists sendTimeout) then DeliveryOptions(null, null, sendTimeout) else null; 20 | 21 | shared void sendRequest(JsonObject request) { 22 | if (exists options) { 23 | eventBus.send(address, request, options); 24 | } 25 | else { 26 | eventBus.send(address, request); 27 | } 28 | } 29 | 30 | shared void sendRepliedRequest(JsonObject request, Anything(Throwable|Message) rep) { 31 | if (exists options) { 32 | eventBus.send(address, request, options, rep); 33 | } 34 | else { 35 | eventBus.send(address, request, rep); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/State.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "Returns timer state by name: 3 | * \"paused\" - timerPaused 4 | * \"running\" - timerRunning 5 | * \"completed\" - timerCompleted 6 | * otherwise - null 7 | " 8 | since( "0.2.0" ) by( "Lis" ) 9 | State? stateByName( String name ) { 10 | if ( name == State.paused.string ) { 11 | return State.paused; 12 | } 13 | else if ( name == State.running.string ) { 14 | return State.running; 15 | } 16 | else if ( name == State.completed.string ) { 17 | return State.completed; 18 | } 19 | else { 20 | return null; 21 | } 22 | } 23 | 24 | 25 | "Timer or scheduler state - running, paused or completed." 26 | tagged( "Info" ) 27 | since( "0.2.0" ) by( "Lis" ) 28 | shared class State of running | paused | completed 29 | { 30 | 31 | shared actual String string; 32 | 33 | "Indicates that the scheduler or timer is in running state." 34 | shared new running { string = Chime.state.running; } 35 | 36 | "Indicates that the scheduler or timer is in paused state." 37 | shared new paused { string = Chime.state.paused; } 38 | 39 | "Indicates that the scheduler or timer has been completed." 40 | shared new completed { string = Chime.state.completed; } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimeServices.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | ObjectValue 3 | } 4 | import herd.schedule.chime.service.message { 5 | MessageSource 6 | } 7 | import ceylon.time { 8 | DateTime 9 | } 10 | import herd.schedule.chime.service.timezone { 11 | TimeZone 12 | } 13 | import herd.schedule.chime.service.producer { 14 | EventProducer 15 | } 16 | 17 | 18 | "Services for the timer or scheduler." 19 | since("0.3.0") by("Lis") 20 | final class TimeServices ( 21 | shared TimeZone timeZone, 22 | shared MessageSource messageSource, 23 | shared EventProducer eventProducer, 24 | shared CalendarService calendar 25 | ) 26 | satisfies TimeZone&MessageSource&EventProducer&CalendarService 27 | { 28 | shared actual void extract(TimerFire event, Anything(ObjectValue?) onMessage) 29 | => messageSource.extract(event, onMessage); 30 | 31 | shared actual Boolean inside(DateTime date) => calendar.inside(date); 32 | 33 | shared actual DateTime nextOutside(DateTime date) => calendar.nextOutside(date); 34 | 35 | shared actual void send(TimerEvent event) => eventProducer.send(event); 36 | 37 | shared actual String timeZoneID => timeZone.timeZoneID; 38 | 39 | shared actual DateTime toLocal(DateTime remote) => timeZone.toLocal(remote); 40 | 41 | shared actual DateTime toRemote(DateTime local) => timeZone.toRemote(local); 42 | 43 | shared actual Boolean calendarIgnorance => calendar.calendarIgnorance; 44 | } 45 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimeUnit.ceylon: -------------------------------------------------------------------------------- 1 | 2 | 3 | "Unit of time." 4 | since( "0.2.1" ) by( "Lis" ) 5 | shared class TimeUnit of hours | minutes | seconds 6 | { 7 | "Number of seconds in the unit." 8 | shared Integer secondsIn; 9 | 10 | "Unit of hour." 11 | shared new hours { secondsIn = 3600; } 12 | "Unit of minutes." 13 | shared new minutes { secondsIn = 60; } 14 | "Unit of seconds." 15 | shared new seconds { secondsIn = 1; } 16 | } 17 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/Timer.ceylon: -------------------------------------------------------------------------------- 1 | 2 | 3 | "Wraps event bus to provide exchanging messages with previously created timer. 4 | The object implementing interface is returned by [[Scheduler.createIntervalTimer]] 5 | [[Scheduler.createCronTimer]], [[Scheduler.createUnionTimer]] and [[Scheduler.createTimer]]. 6 | 7 | Timer is sent timer fire or complete events with [[TimerEvent]]. 8 | To set timer event handler call [[handler]]. 9 | 10 | > Complete event is always published. 11 | 12 | If a timer object is no longer needed call [[unregister]] in order to unregister event listener at event bus. 13 | The listener is automatically unregistered at timer complete event. 14 | " 15 | see( `interface Scheduler` ) 16 | tagged( "Proxy" ) 17 | since( "0.2.0" ) by( "Lis" ) 18 | shared interface Timer { 19 | 20 | "Full name of the timer, i.e. **scheduler name:timer name**." 21 | shared formal String name; 22 | 23 | "Stops and removes this timer." 24 | see( `function Scheduler.deleteTimers` ) 25 | shared formal void delete( "Optional reply handler. Replied with timer name." Anything(Throwable|String)? reply = null ); 26 | 27 | "Pauses this timer." 28 | see( `function resume` ) 29 | shared formal void pause( "Optional reply handler. Replied with timer state." Anything(Throwable|State)? reply = null ); 30 | 31 | "Resumes this timer after pausing." 32 | see( `function pause` ) 33 | shared formal void resume( "Optional reply handler. Replied with timer state." Anything(Throwable|State)? reply = null ); 34 | 35 | "Requests timer info." 36 | see( `function Scheduler.timersInfo` ) 37 | shared formal void info( "Info handler." Anything(Throwable|TimerInfo) info ); 38 | 39 | "Sets the handler for the timer events. Replaces previous one if has been set." 40 | shared formal void handler( Anything(TimerEvent) handler ); 41 | 42 | "Unregister the handler from the event bus, while keep the timer alive." 43 | shared formal void unregister(); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimerContainer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | 3 | JsonObject, 4 | ObjectValue 5 | } 6 | import ceylon.time { 7 | 8 | DateTime 9 | } 10 | import herd.schedule.chime.service.timer { 11 | TimeRow 12 | } 13 | 14 | 15 | "Posses timer." 16 | since("0.1.0") by("Lis") 17 | class TimerContainer ( 18 | "Timer full name, which is **scheduler name:timer name**." shared String name, 19 | "Timer `JsonObject` description" JsonObject description, 20 | "Timer within this container." TimeRow timer, 21 | "Max count or null if not specified." Integer? maxCount, 22 | "Timer start time or null if to be started immediately." DateTime? startTime, 23 | "Timer end time or null if not specified." DateTime? endTime, 24 | "Message to be attached to the timer fire event." ObjectValue? message, 25 | "Timer services: time zone, message source, event producer, calendar." 26 | TimeServices services 27 | ) { 28 | 29 | "Timer fire counting." 30 | shared variable Integer count = 0; 31 | 32 | "Timer state." 33 | shared variable State state = State.paused; 34 | 35 | "Next fire timer in remote TZ or null if completed." 36 | variable DateTime? nextRemoteFireTime = null; 37 | 38 | "Next fire timer in machine local TZ or null if completed." 39 | shared DateTime? localFireTime => if (exists d = nextRemoteFireTime) then services.toLocal(d) else null; 40 | 41 | "Time zone ID." 42 | shared String timeZoneID => services.timeZoneID; 43 | 44 | 45 | "Timer name + state." 46 | shared JsonObject stateDescription() { 47 | return JsonObject { 48 | Chime.key.name -> name, 49 | Chime.key.state -> state.string 50 | }; 51 | } 52 | 53 | "Returns _full_ timer description." 54 | shared JsonObject fullDescription() { 55 | JsonObject descr = JsonObject { 56 | Chime.key.name -> name, 57 | Chime.key.state -> state.string, 58 | Chime.key.count -> count, 59 | Chime.key.description -> description, 60 | Chime.key.timeZone -> timeZoneID 61 | }; 62 | 63 | if (exists d = maxCount) { 64 | descr.put(Chime.key.maxCount, d); 65 | } 66 | 67 | if (exists d = startTime) { 68 | descr.put ( 69 | Chime.key.startTime, 70 | JsonObject { 71 | Chime.date.seconds -> d.seconds, 72 | Chime.date.minutes -> d.minutes, 73 | Chime.date.hours -> d.hours, 74 | Chime.date.dayOfMonth -> d.day, 75 | Chime.date.month -> d.month.string, 76 | Chime.date.year -> d.year 77 | } 78 | ); 79 | } 80 | 81 | if (exists d = endTime) { 82 | descr.put ( 83 | Chime.key.endTime, 84 | JsonObject { 85 | Chime.date.seconds -> d.seconds, 86 | Chime.date.minutes -> d.minutes, 87 | Chime.date.hours -> d.hours, 88 | Chime.date.dayOfMonth -> d.day, 89 | Chime.date.month -> d.month.string, 90 | Chime.date.year -> d.year 91 | } 92 | ); 93 | } 94 | 95 | return descr; 96 | } 97 | 98 | "Creates timer fire event for the next fire date time and using extracted message." 99 | shared void timerFireEvent() { 100 | if (state == State.running, exists at = nextRemoteFireTime) { 101 | TimerFire event = TimerFire(name, count, timeZoneID, at, message); 102 | services.extract(event, sendTimerFireEvent(event)); 103 | } 104 | } 105 | 106 | void sendTimerFireEvent(TimerFire event)(ObjectValue? message) 107 | => services.send(TimerFire(event.timerName, event.count, event.timeZone, event.date, message)); 108 | 109 | 110 | "Starts the timer." 111 | shared void start(DateTime currentLocal) { 112 | DateTime currentRemote = services.toRemote(currentLocal); 113 | // check if max count has been reached before 114 | if (exists c = maxCount) { 115 | if (count >= c) { 116 | complete(); 117 | return; 118 | } 119 | } 120 | // check if start time is after current 121 | DateTime beginning; 122 | if (exists st = startTime) { 123 | if (st > currentRemote) { 124 | beginning = st; 125 | } 126 | else { 127 | beginning = currentRemote; 128 | } 129 | } 130 | else { 131 | beginning = currentRemote; 132 | } 133 | // start timer 134 | if (exists date = calendarDate(timer.start(beginning))) { 135 | if (exists ed = endTime) { 136 | if (date > ed) { 137 | complete(); 138 | return; 139 | } 140 | } 141 | state = State.running; 142 | count ++; 143 | nextRemoteFireTime = date; 144 | } 145 | else { 146 | complete(); 147 | } 148 | } 149 | 150 | "Sets timer completed." 151 | shared void complete() { 152 | nextRemoteFireTime = null; 153 | state = State.completed; 154 | services.send(TimerCompleted(name, count)); 155 | } 156 | 157 | "Shifts timer to the next time." 158 | shared void shiftTime() { 159 | if (state == State.running) { 160 | if (exists date = calendarDate(timer.shiftTime())) { 161 | // check on complete 162 | if (exists ed = endTime) { 163 | if (date > ed) { 164 | complete(); 165 | return; 166 | } 167 | } 168 | if (exists c = maxCount) { 169 | if (count >= c) { 170 | complete(); 171 | return; 172 | } 173 | } 174 | count ++; 175 | nextRemoteFireTime = date; 176 | } 177 | else { 178 | complete(); 179 | } 180 | } 181 | } 182 | 183 | "Returns date bounds by calendar." 184 | DateTime? calendarDate(variable DateTime? date) { 185 | while (exists cur = date) { 186 | if (services.inside(cur)) { 187 | if (services.calendarIgnorance) { 188 | date = timer.shiftTime(); 189 | } 190 | else { 191 | date = services.nextOutside(cur); 192 | } 193 | } 194 | else { 195 | return cur; 196 | } 197 | } 198 | return null; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimerCreator.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | 3 | JsonObject 4 | } 5 | import ceylon.time { 6 | 7 | dateTime, 8 | DateTime 9 | } 10 | import herd.schedule.chime.service { 11 | ChimeServices 12 | } 13 | import herd.schedule.chime.service.timer { 14 | TimeRowFactory, 15 | TimeRow 16 | } 17 | import herd.schedule.chime.cron { 18 | 19 | calendar 20 | } 21 | 22 | 23 | "Uses `JSON` description to creates [[TimerContainer]] with timer [[TimeRow]] created by timer factory." 24 | see(`interface TimeRowFactory`, `interface TimeRow`, `class TimerContainer`) 25 | since("0.1.0") by("Lis") 26 | class TimerCreator ( 27 | "Services Chime provides." ChimeServices services 28 | ) { 29 | 30 | "Creates timer from creation request." 31 | shared TimerContainer|String> createTimer ( 32 | "Timer name." String name, 33 | "Request with timer description." JsonObject request, 34 | "Default time services." TimeServices defaultServices 35 | ) { 36 | if (is JsonObject description = request[Chime.key.description]) { 37 | value timer = services.createTimeRow(description); 38 | if (is TimeRow timer) { 39 | return createTimerContainer(request, description, name, timer, defaultServices); 40 | } 41 | else { 42 | return timer; 43 | } 44 | } 45 | else { 46 | // timer description to be specified 47 | return Chime.errors.codeTimerDescriptionHasToBeSpecified->Chime.errors.timerDescriptionHasToBeSpecified; 48 | } 49 | } 50 | 51 | 52 | "Creates timer container by container and creation request." 53 | TimerContainer|String> createTimerContainer ( 54 | "Request on timer creation." JsonObject request, 55 | "Timer description." JsonObject description, 56 | "Timer name." String name, 57 | "Timer." TimeRow timer, 58 | "Default time services." TimeServices defaultServices 59 | ) { 60 | // extract start date if exists 61 | DateTime? startDate; 62 | if (is JsonObject startTime = request[Chime.key.startTime]) { 63 | if (exists st = extractDate(startTime)) { 64 | startDate = st; 65 | } 66 | else { 67 | return Chime.errors.codeIncorrectStartDate->Chime.errors.incorrectStartDate; 68 | } 69 | } 70 | else { 71 | startDate = null; 72 | } 73 | 74 | // extract end date if exists 75 | DateTime? endDate; 76 | if (is JsonObject endTime = request[Chime.key.endTime]) { 77 | if (exists st = extractDate(endTime)) { 78 | endDate = st; 79 | } 80 | else { 81 | return Chime.errors.codeIncorrectEndDate->Chime.errors.incorrectEndDate; 82 | } 83 | } 84 | else { 85 | endDate = null; 86 | } 87 | 88 | // end date has to be after start! 89 | if (exists st = startDate, exists et = endDate) { 90 | if (et <= st) { 91 | return Chime.errors.codeEndDateToBeAfterStartDate->Chime.errors.endDateToBeAfterStartDate; 92 | } 93 | } 94 | 95 | value exactServices = servicesFromRequest ( 96 | request, services, defaultServices 97 | ); 98 | if (is TimeServices exactServices) { 99 | return TimerContainer ( 100 | name, description, timer, 101 | extractMaxCount(request), startDate, endDate, 102 | request.get(Chime.key.message), exactServices 103 | ); 104 | } 105 | else { 106 | return exactServices; 107 | } 108 | } 109 | 110 | "Extracts month from field with key key. The field can be either integer or string (like JAN, FEB etc, see [[calendar]])." 111 | Integer? extractMonth(JsonObject description, String key) { 112 | if (is Integer val = description[key]) { 113 | if (val > 0 && val < 13) { 114 | return val; 115 | } 116 | else { 117 | return null; 118 | } 119 | } 120 | else if (is String val = description[key]) { 121 | if (exists ret = calendar.monthFullMap[val]) { 122 | return ret; 123 | } 124 | return calendar.monthShortMap[val]; 125 | } 126 | else { 127 | return null; 128 | } 129 | } 130 | 131 | "Extracts date from `JSON`, key returns `JSON` object with date." 132 | DateTime? extractDate(JsonObject date) { 133 | if (is Integer seconds = date[Chime.date.seconds], 134 | is Integer minutes = date[Chime.date.minutes], 135 | is Integer hours = date[Chime.date.hours], 136 | is Integer dayOfMonth = date[Chime.date.dayOfMonth], 137 | is Integer year = date[Chime.date.year], 138 | exists month = extractMonth(date, Chime.date.month) 139 | ) { 140 | try { 141 | return dateTime(year, month, dayOfMonth, hours, minutes, seconds); 142 | } 143 | catch (Throwable err) { 144 | return null; 145 | } 146 | } 147 | return null; 148 | } 149 | 150 | "`maxCount` - nonmandatory field, if not specified - infinitely." 151 | Integer? extractMaxCount(JsonObject description) { 152 | if (is Integer c = description[Chime.key.maxCount]) { 153 | if (c > 0) { 154 | return c; 155 | } 156 | else { 157 | return 1; 158 | } 159 | } 160 | else { 161 | return null; 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimerEvent.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import ceylon.json { 5 | JsonObject, 6 | ObjectValue 7 | } 8 | 9 | 10 | "Represents a timer event: fire or complete. 11 | Timer publishes or sends the event in `JSON` format to timer address when the timer fires or completes. 12 | [[Timer]] interface converts `JsonObject` event into `TimerEvent`. 13 | 14 | > Complete event is always published. 15 | " 16 | see(`interface Timer`, `function Timer.handler`) 17 | tagged("Event") 18 | since("0.2.0") by("Lis") 19 | shared abstract class TimerEvent ( 20 | "Name of the timer which sent the message." 21 | shared String timerName, 22 | "Total number of fires." 23 | shared Integer count 24 | ) 25 | of TimerFire | TimerCompleted 26 | { 27 | "Writes the event into `JsonObject`." 28 | shared formal JsonObject toJson(); 29 | } 30 | 31 | 32 | "Timer fire event." 33 | see(`interface Timer`, `function Timer.handler`) 34 | tagged("Event") 35 | since("0.2.0") by("Lis") 36 | shared final class TimerFire ( 37 | "Name of the timer which fires the message." 38 | String timerName, 39 | "Total number of fires." 40 | Integer count, 41 | "Time zone ID." 42 | shared String timeZone, 43 | "Date the fire is occured at." 44 | shared DateTime date, 45 | "Optional message attached to the timer fire event." 46 | shared ObjectValue? message 47 | ) 48 | extends TimerEvent(timerName, count) 49 | { 50 | 51 | shared actual JsonObject toJson() { 52 | value ret = JsonObject { 53 | Chime.key.event -> Chime.event.fire, 54 | Chime.key.name -> timerName, 55 | Chime.key.count -> count, 56 | Chime.key.time -> date.string, 57 | Chime.date.seconds -> date.seconds, 58 | Chime.date.minutes -> date.minutes, 59 | Chime.date.hours -> date.hours, 60 | Chime.date.dayOfMonth -> date.day, 61 | Chime.date.month -> date.month.integer, 62 | Chime.date.year -> date.year, 63 | Chime.key.timeZone -> timeZone 64 | }; 65 | if ( exists msg = message ) { 66 | ret.put( Chime.key.message, msg ); 67 | } 68 | return ret; 69 | } 70 | 71 | 72 | shared actual String string { 73 | String ret = "fire event of ``timerName`` at ``date`` with total fires of ``count``"; 74 | if (exists msg = message) { 75 | return ret + " and message ``msg``"; 76 | } 77 | else { 78 | return ret; 79 | } 80 | } 81 | } 82 | 83 | 84 | "Timer complete event." 85 | see(`interface Timer`, `function Timer.handler`) 86 | tagged("Event") 87 | since("0.2.0") by("Lis") 88 | shared final class TimerCompleted ( 89 | "Name of the timer which fires the message." 90 | String timerName, 91 | "Total number of fires." 92 | Integer count 93 | ) 94 | extends TimerEvent(timerName, count) 95 | { 96 | shared actual JsonObject toJson() 97 | => JsonObject { 98 | Chime.key.event -> Chime.event.complete, 99 | Chime.key.name -> timerName, 100 | Chime.key.count -> count 101 | }; 102 | shared actual String string => "complete event of ``timerName`` with total fires of ``count``"; 103 | } 104 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimerImpl.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core.eventbus { 2 | EventBus, 3 | MessageConsumer, 4 | Message 5 | } 6 | import ceylon.json { 7 | JsonObject 8 | } 9 | 10 | 11 | "Internal implementation of [[Timer]]." 12 | see(`interface Scheduler`, `class SchedulerImpl`) 13 | since("0.2.0") by("Lis") 14 | class TimerImpl extends Sender satisfies Timer 15 | { 16 | 17 | shared static void replyWithInfo( Anything(Throwable|TimerInfo[]) info) 18 | (Throwable|Message msg) 19 | { 20 | if (is Message msg) { 21 | "Reply from scheduler request has not to be null." 22 | assert (exists ret = msg.body()); 23 | info(ret.getArray(Chime.key.timers).narrow().map(TimerInfo.fromJSON).sequence()); 24 | } 25 | else { 26 | info(msg); 27 | } 28 | } 29 | 30 | 31 | variable Anything(TimerEvent)? eventHandler = null; 32 | MessageConsumer consumer; 33 | variable Boolean alive = true; 34 | shared actual String name; 35 | 36 | shared new (String name, String schedulerAddress, EventBus eventBus, Integer? sendTimeout) 37 | extends Sender(schedulerAddress, eventBus, sendTimeout) 38 | { 39 | this.name = name; 40 | consumer = eventBus.consumer(name); 41 | } 42 | 43 | 44 | "Redirects message to `eventHandler`." 45 | void onMessage(Message message) { 46 | if (exists h = eventHandler) { 47 | "Message from timer has to containe body." 48 | assert (exists body = message.body()); 49 | value eventType = body.getString(Chime.key.event); 50 | if (eventType == Chime.event.fire) { 51 | h ( 52 | TimerFire ( 53 | name, body.getInteger(Chime.key.count), 54 | body.getString(Chime.key.timeZone), 55 | dateTimeFromJSON(body), 56 | body.get(Chime.key.message) 57 | ) 58 | ); 59 | } 60 | else if (eventType == Chime.event.complete) { 61 | unregister(); 62 | alive = false; 63 | h(TimerCompleted(name, body.getInteger(Chime.key.count))); 64 | } 65 | else { 66 | throw AssertionError("timer event has to be one of 'fire' or 'complete'"); 67 | } 68 | } 69 | } 70 | 71 | 72 | shared actual void handler(Anything(TimerEvent) handler) { 73 | if (alive) { 74 | eventHandler = handler; 75 | if (!consumer.isRegistered()) { 76 | consumer.handler(onMessage); 77 | } 78 | } 79 | } 80 | 81 | shared actual void unregister() { 82 | consumer.unregister(); 83 | eventHandler = null; 84 | } 85 | 86 | shared actual void delete(Anything(Throwable|String)? reply) { 87 | if (alive) { 88 | unregister(); 89 | alive = false; 90 | if (exists reply) { 91 | sendRepliedRequest ( 92 | JsonObject { 93 | Chime.key.operation -> Chime.operation.delete, 94 | Chime.key.name -> name 95 | }, 96 | SchedulerImpl.replyWithName(reply) 97 | ); 98 | } 99 | else { 100 | sendRequest ( 101 | JsonObject { 102 | Chime.key.operation -> Chime.operation.delete, 103 | Chime.key.name -> name 104 | } 105 | ); 106 | } 107 | } 108 | } 109 | 110 | shared actual void pause(Anything(Throwable|State)? reply) { 111 | if (alive) { 112 | if (exists reply) { 113 | sendRepliedRequest ( 114 | JsonObject { 115 | Chime.key.operation -> Chime.operation.state, 116 | Chime.key.name -> name, 117 | Chime.key.state -> Chime.state.paused 118 | }, 119 | SchedulerImpl.replyWithState(reply) 120 | ); 121 | } 122 | else { 123 | sendRequest ( 124 | JsonObject { 125 | Chime.key.operation -> Chime.operation.state, 126 | Chime.key.name -> name, 127 | Chime.key.state -> Chime.state.paused 128 | } 129 | ); 130 | } 131 | } 132 | } 133 | 134 | shared actual void resume(Anything(Throwable|State)? reply) { 135 | if (alive) { 136 | if (exists reply) { 137 | sendRepliedRequest ( 138 | JsonObject { 139 | Chime.key.operation -> Chime.operation.state, 140 | Chime.key.name -> name, 141 | Chime.key.state -> Chime.state.running 142 | }, 143 | SchedulerImpl.replyWithState(reply) 144 | ); 145 | } 146 | else { 147 | sendRequest ( 148 | JsonObject { 149 | Chime.key.operation -> Chime.operation.state, 150 | Chime.key.name -> name, 151 | Chime.key.state -> Chime.state.running 152 | } 153 | ); 154 | } 155 | } 156 | } 157 | 158 | shared actual void info("Info handler." Anything(Throwable|TimerInfo) info) { 159 | sendRepliedRequest ( 160 | JsonObject { 161 | Chime.key.operation -> Chime.operation.info, 162 | Chime.key.name -> name 163 | }, 164 | (Throwable|Message msg) { 165 | if (is Message msg) { 166 | "Reply from scheduler request has not to be null." 167 | assert (exists ret = msg.body()); 168 | info( TimerInfo.fromJSON( ret )); 169 | } 170 | else { 171 | info(msg); 172 | } 173 | } 174 | ); 175 | } 176 | 177 | shared actual String string => "Timer ``name``"; 178 | 179 | } 180 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/TimerInfo.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | 3 | JsonObject 4 | } 5 | import ceylon.time { 6 | 7 | DateTime 8 | } 9 | 10 | 11 | "Info on the timer." 12 | see(`interface Timer`, `class SchedulerInfo`, `function Scheduler.info`) 13 | tagged("Info") 14 | since("0.2.0") by("Lis") 15 | shared final class TimerInfo { 16 | 17 | "Timer name" shared String name; 18 | "Timer state at the request moment." shared State state; 19 | "Number of fires at the request moment." shared Integer count; 20 | "Maximum allowed number of fires or `null` if unlimited." shared Integer? maxCount; 21 | "Time the timer has to be started, or `null` if immediately." shared DateTime? startTime; 22 | "Optional time the timer has to be completed." shared DateTime? endTime; 23 | "Time zone the timer works in." shared String timeZone; 24 | "Timer description." shared JsonObject description; 25 | 26 | 27 | "Instantiates `TimerInfo` with the given parameters." 28 | shared new ( 29 | "Timer name" String name, 30 | "Timer state at the request moment." State state, 31 | "Number of fires at the request moment." Integer count, 32 | "Maximum allowed number of fires or `null` if unlimited." Integer? maxCount, 33 | "Time the timer has to be started, or `null` if immediately." DateTime? startTime, 34 | "Optional time the timer has to be completed." DateTime? endTime, 35 | "Time zone the timer works in." String timeZone, 36 | "Timer description." JsonObject description 37 | ) { 38 | this.name = name; 39 | this.state = state; 40 | this.count = count; 41 | this.maxCount = maxCount; 42 | this.startTime = startTime; 43 | this.endTime = endTime; 44 | this.timeZone = timeZone; 45 | this.description = description; 46 | } 47 | 48 | "Instantiates `TimerInfo` from `JsonObject` description as send by _Chime_." 49 | shared new fromJSON("Timer info received from _Chime_." JsonObject timerInfo) { 50 | this.name = timerInfo.getString(Chime.key.name); 51 | "Timer info replied from scheduler has to contain state field." 52 | assert (exists state = stateByName(timerInfo.getString(Chime.key.state))); 53 | this.state = state; 54 | this.count = timerInfo.getInteger(Chime.key.count); 55 | this.maxCount = timerInfo.getIntegerOrNull(Chime.key.maxCount); 56 | this.startTime = 57 | if (exists startTimeDescr = timerInfo.getObjectOrNull(Chime.key.startTime)) 58 | then dateTimeFromJSON(startTimeDescr) 59 | else null; 60 | this.endTime = 61 | if (exists endTimeDescr = timerInfo.getObjectOrNull(Chime.key.endTime)) 62 | then dateTimeFromJSON(endTimeDescr) 63 | else null; 64 | this.timeZone = timerInfo.getString(Chime.key.timeZone); 65 | this.description = timerInfo.getObject(Chime.key.description); 66 | } 67 | 68 | "Name of the scheduler the timer works within." 69 | shared String schedulerName => name[... (name.firstOccurrence( Chime.configuration.nameSeparatorChar) else 0) - 1]; 70 | 71 | "Short name of the timer, i.e. name with shceduler name skipped." 72 | shared String shortName => name[(name.firstOccurrence(Chime.configuration.nameSeparatorChar) else 0) + 1 ...]; 73 | 74 | shared actual String string => "Info on timer ``name``, ``state``, count = ``count``"; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/UnionBuilder.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | ArrayList 3 | } 4 | import ceylon.json { 5 | 6 | JsonObject, 7 | JsonArray 8 | } 9 | import ceylon.time { 10 | DateTime, 11 | Time 12 | } 13 | import ceylon.time.base { 14 | DayOfWeek, 15 | Month 16 | } 17 | import herd.schedule.chime.cron { 18 | calendar, 19 | cron 20 | } 21 | 22 | 23 | "Builds an union timer. 24 | The builder has a number of function to add particular timer. 25 | The function may be called in any order and any number of times. 26 | Finally, [[UnionBuilder.build]] has to be called to build the timer `JsonObject` description. 27 | " 28 | tagged("Builder") 29 | see(`class CronBuilder`, `function package.every`) 30 | since("0.2.1") by("Lis") 31 | shared final class UnionBuilder 32 | { 33 | 34 | ArrayList union; 35 | 36 | 37 | "Instantiates new empty cron builder." 38 | shared new () { 39 | union = ArrayList(); 40 | } 41 | 42 | "Instatiates new builder and copy data from `other`." 43 | shared new fromBuilder("Builder to copy data from." UnionBuilder other) { 44 | union = ArrayList{for(item in other.union) item.clone()}; 45 | } 46 | 47 | 48 | "Builds the timer." 49 | shared JsonObject build() 50 | => JsonObject { 51 | Chime.key.type -> Chime.type.union, 52 | Chime.key.timers -> JsonArray(union) 53 | }; 54 | 55 | 56 | "Adds timer by its `JSON` description." 57 | shared void withTimer("Timer description to be added." JsonObject timer) => union.add(timer); 58 | 59 | "Fires at the given date / time with year taken into account. 60 | So, this timer will fire just a once." 61 | shared void at("Date / time to fire at." DateTime time) 62 | => union.add ( 63 | JsonObject { 64 | Chime.key.type -> Chime.type.cron, 65 | Chime.date.seconds -> time.seconds.string, 66 | Chime.date.minutes -> time.minutes.string, 67 | Chime.date.hours -> time.hours.string, 68 | Chime.date.daysOfMonth -> time.day.string, 69 | Chime.date.months -> time.month.string 70 | } 71 | ); 72 | 73 | "Fires every year at the given time, day and month." 74 | shared void annual ( 75 | "Time to fire at." Time time, 76 | "Day of month to fire at." Integer dayOfMonth, 77 | "Month to fire at." Integer|String|Month month, 78 | "Optional list of days of week to limit fire event to. 79 | > Sunday is the first day of week." 80 | {Integer|String|DayOfWeek+} daysOfWeek = 1..7 81 | ) => union.add ( 82 | JsonObject { 83 | Chime.key.type -> Chime.type.cron, 84 | Chime.date.seconds -> time.seconds.string, 85 | Chime.date.minutes -> time.minutes.string, 86 | Chime.date.hours -> time.hours.string, 87 | Chime.date.daysOfMonth -> dayOfMonth.string, 88 | Chime.date.months -> calendar.digitalMonth(month).string, 89 | Chime.date.daysOfWeek -> calendar.cronDaysOfWeek(daysOfWeek), 90 | Chime.date.years -> cron.allValues.string 91 | } 92 | ); 93 | 94 | "Fires every month at the given time and day of month. 95 | I.e. `monthly(time(0,0,0), 1)` will fire each 1st day of every month at 0:0:0." 96 | shared void monthly ( 97 | "Time to fire at." Time time, 98 | "Day of month to fire at." Integer dayOfMonth, 99 | "Optional list of days of week to limit fire event to. 100 | > Sunday is the first day of week." 101 | {Integer|String|DayOfWeek+} daysOfWeek = 1..7 102 | ) => union.add ( 103 | JsonObject { 104 | Chime.key.type -> Chime.type.cron, 105 | Chime.date.seconds -> time.seconds.string, 106 | Chime.date.minutes -> time.minutes.string, 107 | Chime.date.hours -> time.hours.string, 108 | Chime.date.daysOfMonth -> dayOfMonth.string, 109 | Chime.date.months -> cron.allValues.string, 110 | Chime.date.daysOfWeek -> calendar.cronDaysOfWeek(daysOfWeek), 111 | Chime.date.years -> cron.allValues.string 112 | } 113 | ); 114 | 115 | "Fires every given day of week at the given time. 116 | I.e. `weekly(time(0,0,0), monday)` will fire each monday at 0:0:0 time." 117 | shared void weekly ( 118 | "Time to fire at." Time time, 119 | "Day of week to fire at. 120 | > Sunday is the first day of week." 121 | Integer|String|DayOfWeek dayOfWeek 122 | ) => union.add ( 123 | JsonObject { 124 | Chime.key.type -> Chime.type.cron, 125 | Chime.date.seconds -> time.seconds.string, 126 | Chime.date.minutes -> time.minutes.string, 127 | Chime.date.hours -> time.hours.string, 128 | Chime.date.daysOfMonth -> cron.allValues.string, 129 | Chime.date.months -> cron.allValues.string, 130 | Chime.date.daysOfWeek -> calendar.digitalDayOfWeek(dayOfWeek).string, 131 | Chime.date.years -> cron.allValues.string 132 | } 133 | ); 134 | 135 | "Fires every month at the last day of week and given time. 136 | I.e. `last(time(0,0,0), friday)` will fire every month last friday at 0:0:0 time." 137 | shared void last ( 138 | "Time to fire at." Time time, 139 | "Last day of week to fire at. 140 | > Sunday is the first day of week." 141 | Integer|String|DayOfWeek dayOfWeek 142 | ) => union.add ( 143 | JsonObject { 144 | Chime.key.type -> Chime.type.cron, 145 | Chime.date.seconds -> time.seconds.string, 146 | Chime.date.minutes -> time.minutes.string, 147 | Chime.date.hours -> time.hours.string, 148 | Chime.date.daysOfMonth -> cron.allValues.string, 149 | Chime.date.months -> cron.allValues.string, 150 | Chime.date.daysOfWeek -> calendar.digitalDayOfWeek(dayOfWeek).string + cron.last.string, 151 | Chime.date.years -> cron.allValues.string 152 | } 153 | ); 154 | 155 | "Fires every month at the n'th day of week and given time. 156 | I.e. `last(time(0,0,0), friday, 3)` will fire every month third friday at 0:0:0 time." 157 | shared void ordered ( 158 | "Time to fire at." Time time, 159 | "Day of week to fire at. 160 | > Sunday is the first day of week." 161 | Integer|String|DayOfWeek dayOfWeek, 162 | "Theorder of the day of week to fire at." Integer order 163 | ) => union.add ( 164 | JsonObject { 165 | Chime.key.type -> Chime.type.cron, 166 | Chime.date.seconds -> time.seconds.string, 167 | Chime.date.minutes -> time.minutes.string, 168 | Chime.date.hours -> time.hours.string, 169 | Chime.date.daysOfMonth -> cron.allValues.string, 170 | Chime.date.months -> cron.allValues.string, 171 | Chime.date.daysOfWeek -> calendar.digitalDayOfWeek(dayOfWeek).string + cron.nth.string + order.string, 172 | Chime.date.years -> cron.allValues.string 173 | } 174 | ); 175 | 176 | "Fires daily at the given time. 177 | For example, `daily(Time(12,0,0), 1..5)` will fire at 12:0:0 at working days only." 178 | shared void daily ( 179 | "Time to fire at." Time time, 180 | "Optional list of days of week to limit fire event to. 181 | > Sunday is the first day of week." 182 | {Integer|String|DayOfWeek+} daysOfWeek = 1..7 183 | ) => union.add ( 184 | JsonObject { 185 | Chime.key.type -> Chime.type.cron, 186 | Chime.date.seconds -> time.seconds.string, 187 | Chime.date.minutes -> time.minutes.string, 188 | Chime.date.hours -> time.hours.string, 189 | Chime.date.daysOfMonth -> cron.allValues.string, 190 | Chime.date.months -> cron.allValues.string, 191 | Chime.date.daysOfWeek -> calendar.cronDaysOfWeek(daysOfWeek), 192 | Chime.date.years -> cron.allValues.string 193 | } 194 | ); 195 | 196 | "Fires every given time interval." 197 | shared void every ( 198 | "Timer interval measured in `timeUnit`." Integer interval, 199 | "Unit to measure `delay`." TimeUnit timeUnit = TimeUnit.seconds 200 | ) { 201 | "Timer interval has to be positive, while given is ``interval``." 202 | assert(interval > 0); 203 | union.add ( 204 | JsonObject { 205 | Chime.key.type -> Chime.type.interval, 206 | Chime.key.delay -> interval * timeUnit.secondsIn 207 | } 208 | ); 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/calendarBuilder.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time.base { 2 | Month, 3 | DayOfWeek 4 | } 5 | import ceylon.json { 6 | JsonObject, 7 | JsonArray 8 | } 9 | import herd.schedule.chime.service.calendar { 10 | AnnuallyFactory, 11 | DailyFactory, 12 | DativityFactory, 13 | LastDayOfWeekFactory, 14 | MonthlyFactory, 15 | WeekdayOfMonthFactory, 16 | WeeklyFactory, 17 | IntersectionFactory, 18 | UnionFactory 19 | } 20 | import ceylon.time { 21 | Time 22 | } 23 | 24 | 25 | "Builds calendar Json descriptions." 26 | tagged("Builder") 27 | see(`package herd.schedule.chime.service.calendar`) 28 | since("0.3.0") by("Lis") 29 | shared object calendar 30 | { 31 | 32 | "Builds annually calendar Json desciption. 33 | Annually calendar excludes a given list of months." 34 | see(`class AnnuallyFactory`) 35 | shared JsonObject annually("Months the calendar excludes." Month+ months) 36 | => JsonObject { 37 | Chime.calendar.type -> Chime.calendar.annually, 38 | Chime.date.months -> JsonArray {for (item in months) item.integer} 39 | }; 40 | 41 | "Builds daily calendar Json description. 42 | Daily calendar excludes _from - to_ time of day." 43 | see(`class DailyFactory`) 44 | shared JsonObject daily ( 45 | "Time the calendar excludes from." Time from, 46 | "Time the calendar excludes to." Time to, 47 | "Tolerance in second applied to shift the calendar out the restrictions, default is 1s." Integer tolerance = 1 48 | ) { 49 | value ret = JsonObject { 50 | Chime.calendar.type -> Chime.calendar.daily, 51 | Chime.calendar.from -> JsonObject { 52 | Chime.date.seconds -> from.seconds, 53 | Chime.date.minutes -> from.minutes, 54 | Chime.date.hours -> from.hours 55 | }, 56 | Chime.calendar.to -> JsonObject { 57 | Chime.date.seconds -> to.seconds, 58 | Chime.date.minutes -> to.minutes, 59 | Chime.date.hours -> to.hours 60 | } 61 | }; 62 | if (tolerance > 0) { 63 | ret.put(Chime.calendar.tolerance, tolerance); 64 | } 65 | return ret; 66 | } 67 | 68 | "Builds dativity calendar Json desciption. 69 | Dativity calendar excludes a given list of dates." 70 | see(`class DativityFactory`) 71 | shared JsonObject dativity ( 72 | "Dates calendar excludes. 73 | If it is `[Integer, Month]` then date with the given day and month and with any year is excluded. 74 | If it is `[Integer, Month, Integer]` then date with the given day, month and year is excluded" 75 | [Integer, Month]|[Integer, Month, Integer]+ dates 76 | ) 77 | => JsonObject { 78 | Chime.calendar.type -> Chime.calendar.dativity, 79 | Chime.calendar.dates -> JsonArray { 80 | for (item in dates) if (is [Integer, Month] item) then 81 | JsonObject { 82 | Chime.date.dayOfMonth -> item[0], 83 | Chime.date.month -> item[1].integer 84 | } 85 | else 86 | JsonObject { 87 | Chime.date.dayOfMonth -> item[0], 88 | Chime.date.month -> item[1].integer, 89 | Chime.date.year -> item[2] 90 | } 91 | } 92 | }; 93 | 94 | "Builds last day of week calendar Json description. 95 | Last day of week calendar excludes only a last day of week in each month." 96 | see(`class LastDayOfWeekFactory`) 97 | shared JsonObject lastDayOfWeek("Day of week calenda excludes. Only last day of week." DayOfWeek dayOfWeek) 98 | => JsonObject { 99 | Chime.calendar.type -> Chime.calendar.lastDayOfWeek, 100 | Chime.date.dayOfWeek -> dayOfWeek.successor.integer 101 | }; 102 | 103 | "Builds monthly calendar Json description. 104 | Monthly calendar excludes a given list of days for every month." 105 | see(`class MonthlyFactory`) 106 | shared JsonObject monthly("Days calendar excludes." Integer+ days) 107 | => JsonObject { 108 | Chime.calendar.type -> Chime.calendar.monthly, 109 | Chime.date.months -> JsonArray(days) 110 | }; 111 | 112 | "Builds weekday of month calendar Json description. 113 | Weekday of month calendar excludes a given weekday with the given order in each month (i.e. excludes nth weekday). 114 | " 115 | see(`class WeekdayOfMonthFactory`) 116 | shared JsonObject weekdayOfMonth ( 117 | "Day of week to be excluded." DayOfWeek dayOfWeek, 118 | "Order of the excluded day of week." Integer order 119 | ) 120 | => JsonObject { 121 | Chime.calendar.type -> Chime.calendar.weekdayOfMonth, 122 | Chime.date.dayOfWeek -> dayOfWeek.successor.integer, 123 | Chime.date.order -> order 124 | }; 125 | 126 | "Builds weekly calendar Json description. 127 | Weekly calendar excludes a given list of week days every week." 128 | see(`class WeeklyFactory`) 129 | shared JsonObject weekly ( 130 | "Days of week to be excluded." DayOfWeek+ dayOfWeeks 131 | ) 132 | => JsonObject { 133 | Chime.calendar.type -> Chime.calendar.monthly, 134 | Chime.date.months -> JsonArray {for(item in dayOfWeeks) item.successor.integer} 135 | }; 136 | 137 | "Builds calendars intersection." 138 | see(`class IntersectionFactory`) 139 | shared JsonObject intersect(JsonObject+ calendars) 140 | => JsonObject { 141 | Chime.calendar.type -> Chime.calendar.intersection, 142 | Chime.date.months -> JsonArray(calendars) 143 | }; 144 | 145 | "Builds calendars union." 146 | see(`class UnionFactory`) 147 | shared JsonObject union(JsonObject+ calendars) 148 | => JsonObject { 149 | Chime.calendar.type -> Chime.calendar.union, 150 | Chime.date.months -> JsonArray(calendars) 151 | }; 152 | 153 | } 154 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/CronExpression.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "Cron expression parsed by items." 3 | since( "0.1.0" ) by( "Lis" ) 4 | shared class CronExpression ( 5 | "Set of exoression seconds, 0-59." shared Set seconds, 6 | "Set of exoression minutes, 0-59." shared Set minutes, 7 | "Set of exoression hours, 0-23." shared Set hours, 8 | "Set of exoression days of month, 1-31." shared Set daysOfMonth, 9 | "Set of exoression months, 1-12." shared Set months, 10 | "Days of week." shared DaysOfWeekList daysOfWeek, 11 | "Set of exoression years, can be empty." shared Set years 12 | ) 13 | {} 14 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/DaysOrder.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | 3 | Date 4 | } 5 | import ceylon.time.base { 6 | 7 | DayOfWeek 8 | } 9 | 10 | 11 | "Checks order of day of week." 12 | since("0.1.0") by("Lis") 13 | shared interface DayOrder 14 | { 15 | "`true` if data falls on a one of ordered day and `false` otherwise." 16 | shared formal Boolean falls(Date date); 17 | } 18 | 19 | 20 | "Cheks if date falls on a one from day of week list." 21 | since("0.1.0") by("Lis") 22 | shared class DaysOfWeekList("set of day order" shared {DayOrder*} orderedDays) satisfies DayOrder 23 | { 24 | "`true` if one of ordered days returns `true` and `false` if all of them returns `false`." 25 | shared actual Boolean falls(Date date) { 26 | for (item in orderedDays) { 27 | if (item.falls(date)) { 28 | return true; 29 | } 30 | } 31 | return false; 32 | } 33 | } 34 | 35 | 36 | "All days are accepted." 37 | since("0.1.0") by("Lis") 38 | class DayOrderAll() satisfies DayOrder 39 | { 40 | shared actual Boolean falls(Date date) => true; 41 | } 42 | 43 | "Checks if data falls on one of specified day of week." 44 | since("0.1.0") by("Lis") 45 | class DayOrderWeek("Set of accepted days of week, if empty all days are rejected." shared Set daysOfWeek) 46 | satisfies DayOrder 47 | { 48 | shared actual Boolean falls(Date date) => daysOfWeek.contains(date.dayOfWeek); 49 | } 50 | 51 | "Checks if date is nth day of week." 52 | since("0.1.0") by("Lis") 53 | class DayOrderNth("Accepted day of week." shared DayOfWeek day, "'nth' order of day of week." Integer order) 54 | satisfies DayOrder 55 | { 56 | shared actual Boolean falls(Date date) => date.dayOfWeek == day && order == (date.day - 1) / 7 + 1; 57 | } 58 | 59 | "Checks if date is last day of week in the month." 60 | since("0.1.0") by("Lis") 61 | class DayOrderLast("Accepted day of week." shared DayOfWeek day) 62 | satisfies DayOrder 63 | { 64 | shared actual Boolean falls(Date date) => date.dayOfWeek == day && date.plusDays(7).month != date.month; 65 | } 66 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/calendar.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time.base { 2 | DayOfWeek, 3 | Month, 4 | weekdays 5 | } 6 | import ceylon.json { 7 | JsonObject 8 | } 9 | 10 | 11 | "Defines calendar constants." 12 | since( "0.1.0" ) by( "Lis" ) 13 | shared object calendar 14 | { 15 | 16 | "Mapping month short name to month id." 17 | shared Map monthShortMap = 18 | map { 19 | "JAN" -> 1, 20 | "FEB" -> 2, 21 | "MAR" -> 3, 22 | "APR" -> 4, 23 | "MAY" -> 5, 24 | "JUN" -> 6, 25 | "JUL" -> 7, 26 | "AUG" -> 8, 27 | "SEP" -> 9, 28 | "OCT" -> 10, 29 | "NOV" -> 11, 30 | "DEC" -> 12 31 | }; 32 | 33 | "Mapping month name to month id." 34 | shared Map monthFullMap = 35 | map { 36 | "JANUARY" -> 1, 37 | "FEBRUARY" -> 2, 38 | "MARCH" -> 3, 39 | "APRIL" -> 4, 40 | "MAY" -> 5, 41 | "JUNE" -> 6, 42 | "JULY" -> 7, 43 | "AUGUST" -> 8, 44 | "SEPTEMBER" -> 9, 45 | "OCTOBER" -> 10, 46 | "NOVEMBER" -> 11, 47 | "DECEMBER" -> 12 48 | }; 49 | 50 | "Mapping day of week short name to day of week id." 51 | shared Map dayOfWeekShortMap = 52 | map { 53 | "SUN" -> 1, 54 | "MON" -> 2, 55 | "TUE" -> 3, 56 | "WED" -> 4, 57 | "THU" -> 5, 58 | "FRI" -> 6, 59 | "SAT" -> 7 60 | }; 61 | 62 | "Mapping day of week name to day of week id." 63 | shared Map dayOfWeekFullMap = 64 | map { 65 | "SUNDAY" -> 1, 66 | "MONDAY" -> 2, 67 | "TUESDAY" -> 3, 68 | "WEDNESDAY" -> 4, 69 | "THURSDAY" -> 5, 70 | "FRIDAY" -> 6, 71 | "SATURDAY" -> 7 72 | }; 73 | 74 | 75 | String replaceStringToNumber(String expression, Map map) { 76 | variable String ret = expression; 77 | for (key -> item in map) { 78 | ret = ret.replace(key, item.string); 79 | } 80 | return ret; 81 | } 82 | 83 | "Replace all occurancies of month names by corresponding number." 84 | shared String replaceMonthByNumber(String expression) 85 | => replaceStringToNumber(replaceStringToNumber(expression.trimmed.uppercased, monthFullMap), monthShortMap); 86 | 87 | "Replace all occurancies of weekday names by corresponding number." 88 | shared String replaceDayOfWeekByNumber(String expression) 89 | => replaceStringToNumber(replaceStringToNumber(expression.trimmed.uppercased, dayOfWeekFullMap), dayOfWeekShortMap); 90 | 91 | 92 | "Integer representation of a day of week." 93 | shared Integer digitalDayOfWeek(Integer|DayOfWeek|String dayOfWeek) { 94 | switch (dayOfWeek) 95 | case (is Integer) { 96 | "Has to be a valid day of week. Actually is ``dayOfWeek``." 97 | assert (dayOfWeek > 0 && dayOfWeek < 8); 98 | return dayOfWeek; 99 | } 100 | case (is DayOfWeek) { 101 | return dayOfWeek.successor.integer; 102 | } 103 | case (is String) { 104 | "Has to be a valid day of week. Actually is ``dayOfWeek``." 105 | assert (is Integer ret = Integer.parse(replaceDayOfWeekByNumber(dayOfWeek))); 106 | return ret; 107 | } 108 | } 109 | 110 | "Day of week from Integer or String." 111 | shared DayOfWeek dayOfWeekFromString(Integer|String dayOfWeek) { 112 | "Has to be a valid day of week. Actually is ``dayOfWeek``." 113 | assert (exists dow = weekdays[digitalDayOfWeek(dayOfWeek) - 1] ); 114 | return dow; 115 | } 116 | 117 | "Day of week from json." 118 | shared DayOfWeek dayOfWeekFromJson(JsonObject descr, String key) { 119 | "Day of week has to be of String or Integer." 120 | assert (is Integer|String m = descr[key]); 121 | return dayOfWeekFromString(m); 122 | } 123 | 124 | "Integer representation of a list of day of week." 125 | shared {Integer+} digitalDaysOfWeekList({Integer|DayOfWeek|String+} daysOfWeek) { 126 | return {for (item in daysOfWeek) digitalDayOfWeek(item)}; 127 | } 128 | 129 | shared String cronDaysOfWeek({Integer|DayOfWeek|String+} daysOfWeek) { 130 | StringBuilder builder = StringBuilder(); 131 | for (item in daysOfWeek.exceptLast) { 132 | builder.append(digitalDayOfWeek(item).string + ","); 133 | } 134 | builder.append(digitalDayOfWeek(daysOfWeek.last).string); 135 | return builder.string; 136 | } 137 | 138 | "Integer month from json." 139 | shared Integer monthFromJson(JsonObject descr, String key) { 140 | "Month has to be of String or Integer." 141 | assert (is Integer|String m = descr[key]); 142 | return digitalMonth(m); 143 | } 144 | 145 | "Integer representation of a month." 146 | shared Integer digitalMonth(Integer|Month|String month) { 147 | switch (month) 148 | case (is Integer) { 149 | "Has to be a valid month. Actually is ``month``." 150 | assert (month > 0 && month < 13); 151 | return month; 152 | } 153 | case (is Month) { 154 | return month.integer; 155 | } 156 | case (is String) { 157 | "Has to be a valid month. Actually is ``month``." 158 | assert (is Integer ret = Integer.parse(calendar.replaceMonthByNumber(month))); 159 | return ret; 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/cronDefinitions.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "defines some constant used within cron expresion" 3 | since( "0.1.0" ) by( "Lis" ) 4 | shared object cron 5 | { 6 | 7 | // cron special symbols 8 | 9 | "separators" 10 | shared {Character*} separators = { ' ', '\t', '\r', '\n' }; 11 | 12 | "delimiter of fields" 13 | shared Character delimiter = ','; 14 | 15 | "special symbols in a one token" 16 | shared {Character*} special = { '/', '-' }; 17 | 18 | "increments symbol" 19 | shared Character increments = '/'; 20 | 21 | "range symbol" 22 | shared Character range = '-'; 23 | 24 | "all values symbol" 25 | shared Character allValues = '*'; 26 | 27 | "last symbol" 28 | shared Character last = 'L'; 29 | 30 | "nth day symbol" 31 | shared Character nth = '#'; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/package.ceylon: -------------------------------------------------------------------------------- 1 | "Parsing cron strings." 2 | since( "0.1.0" ) by( "Lis" ) 3 | package herd.schedule.chime.cron; 4 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/parseCron.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | 3 | today 4 | } 5 | 6 | 7 | "Parses cron expression from strings." 8 | since( "0.1.0" ) by( "Lis" ) 9 | shared CronExpression? parseCron ( 10 | "cron style string with seconds" String seconds, 11 | "cron style string with minutes" String minutes, 12 | "cron style string with hours" String hours, 13 | "cron style string with daysOfMonth" String daysOfMonth, 14 | "cron style string with months" String months, 15 | "cron style string with days of week - optional if not specified all weekdays included" String? daysOfWeek = null, 16 | "cron style string with years - optional if not specified every year included" String? years = null, 17 | "maximum year period." Integer maxYearPeriod = 10 18 | ) { 19 | 20 | // replace month names by numbers 21 | variable String monthsToInt = calendar.replaceMonthByNumber( months ); 22 | 23 | // parse mandatory fields 24 | value secondsSet = parseCronStyle( seconds, 0, 59 ); 25 | value minutesSet = parseCronStyle( minutes, 0, 59 ); 26 | value hoursSet = parseCronStyle( hours, 0, 23 ); 27 | value daysOfMonthSet = parseCronStyle( daysOfMonth, 1, 31 ); 28 | value monthsSet = parseCronStyle( monthsToInt, 1, 12 ); 29 | 30 | if ( !secondsSet.empty && !minutesSet.empty && !hoursSet.empty && !daysOfMonthSet.empty && !monthsSet.empty ) { 31 | 32 | // parse days of week, which is nonmandatory, if doesn't exists all days accepted 33 | DaysOfWeekList daysOfWeekList; 34 | if ( exists strDaysOfWeek = daysOfWeek, !strDaysOfWeek.empty ) { 35 | // replace all weekday names by numbers 36 | variable String weekdayToInt = calendar.replaceDayOfWeekByNumber( strDaysOfWeek ); 37 | // do parsing 38 | if ( exists parsedDaysOfWeek = parseCronDaysOfWeek( weekdayToInt ) ) { 39 | daysOfWeekList = parsedDaysOfWeek; 40 | } 41 | else { 42 | return null; 43 | } 44 | } 45 | else { 46 | daysOfWeekList = DaysOfWeekList( {DayOrderAll()} ); 47 | } 48 | 49 | // parse years, which is nonmandatory, if doesn't exists any year accepted 50 | Set yearsSet; 51 | if ( exists strYears = years, !strYears.empty ) { 52 | Integer todayYear = today().year; 53 | yearsSet = parseCronStyle( strYears, todayYear, todayYear + maxYearPeriod ); 54 | if ( yearsSet.empty ) { 55 | return null; 56 | } 57 | } 58 | else { 59 | yearsSet = emptySet; 60 | } 61 | 62 | return CronExpression( secondsSet, minutesSet, hoursSet, daysOfMonthSet, monthsSet, daysOfWeekList, yearsSet ); 63 | } 64 | else { 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/parseCronDaysOfWeek.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | 3 | ArrayList, 4 | HashSet, 5 | linked, 6 | Hashtable 7 | } 8 | import ceylon.time.base { 9 | 10 | dayOfWeek, 11 | DayOfWeek 12 | } 13 | 14 | 15 | "Parses days of week from string." 16 | since( "0.1.0" ) by( "Lis" ) 17 | DaysOfWeekList? parseCronDaysOfWeek( String expression ) { 18 | 19 | // all values 20 | if ( expression == cron.allValues.string || expression.empty ) { 21 | return DaysOfWeekList( {DayOrderAll()} ); 22 | } 23 | 24 | ArrayList days = ArrayList(); 25 | // parse tokens 26 | {String*} tokens = expression.split( cron.delimiter.equals ).map( String.trimmed ); 27 | for ( token in tokens ) { 28 | if ( exists parsedToken = parseDayOrder( token ) ) { 29 | days.add( parsedToken ); 30 | } 31 | else { 32 | return null; 33 | } 34 | } 35 | 36 | if ( days.empty ) { 37 | return null; 38 | } 39 | else { 40 | return DaysOfWeekList( { for ( item in days ) item } ); 41 | } 42 | 43 | } 44 | 45 | 46 | "Parses day order from string." 47 | by( "Lis" ) 48 | DayOrder? parseDayOrder( String expression ) { 49 | if ( expression.contains( cron.nth ) ) { 50 | {String*} tokens = expression.split( cron.nth.equals ); 51 | if ( tokens.size == 2, 52 | exists weekday = parseStringToInteger( tokens.first ), 53 | exists order = parseStringToInteger( tokens.last ) 54 | ) { 55 | if ( weekday > 0 && weekday < 8 && order > 0 && order < 6 ) { 56 | return DayOrderNth( dayOfWeek( weekday - 1 ), order ); 57 | } 58 | } 59 | } 60 | else if ( exists last = expression.last, last == cron.last ) { 61 | if ( exists day = parseStringToInteger( expression.spanTo( expression.size - 2 ) ) ) { 62 | if ( day > 0 && day < 8 ) { 63 | return DayOrderLast( dayOfWeek( day - 1 ) ); 64 | } 65 | } 66 | } 67 | else { 68 | if ( exists daysSet = parseCronRange( expression, 1, 7 ) ) { 69 | return DayOrderWeek ( 70 | HashSet ( 71 | linked, Hashtable(), daysSet.map( ( Integer element ) => dayOfWeek( element - 1 ) ) 72 | ) 73 | ); 74 | } 75 | } 76 | return null; 77 | } 78 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/parseCronRange.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | 3 | HashSet 4 | } 5 | 6 | 7 | "Parses cron range in format TO-FROM / STEP 8 | where TO, FROM and STEP: 9 | * contains only digits, 10 | * `FROM`-`TO`/`STEP`, `FROM`, `TO` and `STEP` are digits 11 | * `FROM`/`STEP`, `FROM` and `STEP` are digits, TO eqauls to max possible value 12 | * `FROM`-`TO`, FROM and TO are digits, step is supposed to be 1 13 | * TO and FROM have to be greater or equal min and less or equal max 14 | " 15 | since( "0.1.0" ) by( "Lis" ) 16 | Set? parseCronRange( String expression, Integer minValue, Integer maxValue ) { 17 | HashSet ret = HashSet(); 18 | {String*} ranged = expression.split( cron.special.contains, false ).map( String.trimmed ); 19 | if ( exists from = parseStringToInteger( ranged.first ) ) { 20 | variable Integer to = from; 21 | variable Integer step = 1; 22 | if ( ranged.size == 1 ) { 23 | if ( from < minValue || from > maxValue ) { 24 | // 'from' to be within accepted values 25 | return null; 26 | } 27 | } 28 | else if ( ranged.size == 3 ) { 29 | if ( exists del = ranged.getFromFirst( 1 ) ) { 30 | if ( del == cron.range.string ) { 31 | if ( exists parsed = parseStringToInteger( ranged.getFromFirst( 2 ) ) ) { 32 | to = parsed; 33 | if ( to < from || to > maxValue ) { 34 | // 'to' to be within accepted values 35 | return null; 36 | } 37 | } 38 | else { 39 | // not digits 40 | return null; 41 | } 42 | } 43 | else if ( del == cron.increments.string ) { 44 | if ( exists parsed = parseStringToInteger( ranged.getFromFirst( 2 ) ) ) { 45 | if ( parsed < 1 ) { 46 | // step to be greater zero 47 | return null; 48 | } 49 | step = parsed; 50 | to = maxValue; 51 | } 52 | else { 53 | // not digits 54 | return null; 55 | } 56 | } 57 | else { 58 | // only '-' or '/' supported 59 | return null; 60 | } 61 | } 62 | else { 63 | return null; 64 | } 65 | } else if ( ranged.size == 5 ) { 66 | if ( exists del1 = ranged.getFromFirst( 1 ), exists del2 = ranged.getFromFirst( 3 ) ) { 67 | if ( del1 == cron.range.string && del2 == cron.increments.string ) { 68 | if ( exists parsedTo = parseStringToInteger( ranged.getFromFirst( 2 ) ), 69 | exists parsedStep = parseStringToInteger( ranged.getFromFirst( 4 ) ) ) 70 | { 71 | if ( parsedTo < minValue || parsedTo > maxValue || parsedStep < 1 ) { 72 | // incorrect values 73 | return null; 74 | } 75 | to = parsedTo; 76 | step = parsedStep; 77 | } 78 | else { 79 | // not digits 80 | return null; 81 | } 82 | } 83 | else { 84 | // incorrect format - to be X-X/X 85 | return null; 86 | } 87 | } 88 | else { 89 | return null; 90 | } 91 | } 92 | else { 93 | // token must be in format X-X/X 94 | return null; 95 | } 96 | if ( step < 1 ) { 97 | step = 1; 98 | } 99 | // store range into set 100 | variable Integer storing = from; 101 | while ( storing <= to && storing <= maxValue ) { 102 | if ( storing >= minValue ) { 103 | ret.add( storing ); 104 | } 105 | storing += step; 106 | } 107 | if ( ret.empty ) { 108 | return null; 109 | } 110 | else { 111 | return ret; 112 | } 113 | } 114 | return null; 115 | } 116 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/parseCronStyle.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | 3 | HashSet 4 | } 5 | 6 | 7 | "Parses string expression with cron style time, returns set with all possible values. 8 | Supported time in format X,X,... 9 | where X to be: 10 | * digits greater or equal min and less or equal max, 11 | * `FROM`-`TO`/`STEP`, `FROM`, `TO` and `STEP` are digits 12 | * `FROM`/`STEP`, `FROM` and `STEP` are digits, TO eqauls to max possible value 13 | * `FROM`-`TO`, FROM and TO are digits, step is supposed to be 1 14 | " 15 | since( "0.1.0" ) by( "Lis" ) 16 | Set parseCronStyle( 17 | "expression to be parsed" String expression, 18 | "min possible value" Integer minValue, 19 | "max possible value" Integer maxValue ) 20 | { 21 | String trimmedExpr = expression.trimmed; 22 | HashSet ret = HashSet(); 23 | 24 | // all values 25 | if ( trimmedExpr == cron.allValues.string ) { 26 | variable Integer storing = minValue; 27 | while ( storing <= maxValue ) { 28 | ret.add( storing ); 29 | storing ++; 30 | } 31 | return ret; 32 | } 33 | 34 | // parse tokens 35 | {String*} tokens = trimmedExpr.split( cron.delimiter.equals ).map( String.trimmed ); 36 | for ( token in tokens ) { 37 | if ( exists tokenSet = parseCronRange( token, minValue, maxValue ) ) { 38 | ret.addAll( tokenSet ); 39 | } 40 | else { 41 | ret.clear(); 42 | break; 43 | } 44 | } 45 | return ret; 46 | } 47 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/cron/stringToInteger.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "Parses Integer from String?." 3 | since( "0.1.0" ) by( "Lis" ) 4 | Integer? parseStringToInteger( String? str ) { 5 | if ( exists parsing = str ) { 6 | if ( is Integer ret = Integer.parse( parsing ) ) { 7 | return ret; 8 | } 9 | else { 10 | return null; 11 | } 12 | } 13 | else { 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/dateTimeFromJSON.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | JsonObject 3 | } 4 | import ceylon.time { 5 | dateTime, 6 | DateTime 7 | } 8 | import herd.schedule.chime.cron { 9 | calendar 10 | } 11 | 12 | 13 | "Reads date-time from JSON into `DateTime`." 14 | since("0.2.0") by("Lis") 15 | DateTime dateTimeFromJSON(JsonObject dateTimeDescr) 16 | => dateTime ( 17 | dateTimeDescr.getInteger(Chime.date.year), calendar.monthFromJson(dateTimeDescr, Chime.date.month), 18 | dateTimeDescr.getInteger(Chime.date.dayOfMonth), dateTimeDescr.getInteger(Chime.date.hours), 19 | dateTimeDescr.getInteger(Chime.date.minutes), dateTimeDescr.getInteger(Chime.date.seconds) 20 | ); 21 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/every.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | 3 | JsonObject 4 | } 5 | 6 | 7 | "Builds `JsonObject` description of an interval timer." 8 | tagged( "Builder" ) 9 | see( `class CronBuilder`, `class UnionBuilder` ) 10 | since( "0.2.1" ) by( "Lis" ) 11 | shared JsonObject every ( 12 | "Timer interval measured in `timeUnit`." Integer interval, 13 | "Unit to measure `interval`." TimeUnit timeUnit = TimeUnit.seconds 14 | ) { 15 | "Timer interval has to be positive, while given is ``interval``." 16 | assert( interval > 0 ); 17 | 18 | JsonObject ret= JsonObject { 19 | Chime.key.type -> Chime.type.interval, 20 | Chime.key.delay -> interval * timeUnit.secondsIn 21 | }; 22 | return ret; 23 | } 24 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/extractors.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | ChimeServices 3 | } 4 | import herd.schedule.chime.service.message { 5 | MessageSource 6 | } 7 | import herd.schedule.chime.service.timezone { 8 | TimeZone 9 | } 10 | import herd.schedule.chime.service.producer { 11 | EventProducer 12 | } 13 | import herd.schedule.chime.service.calendar { 14 | Calendar 15 | } 16 | import ceylon.json { 17 | JsonObject 18 | } 19 | 20 | 21 | "Extract services (time zone and message source) from timer or scheduler request." 22 | since("0.3.0") by("Lis") 23 | TimeServices|String> servicesFromRequest ( 24 | "Timer description to get time zone name." JsonObject request, 25 | "Services Chime provides." ChimeServices services, 26 | "Default time services." TimeServices defaultServices 27 | ) { 28 | value converter = timeZoneFromRequest(request, services, defaultServices.timeZone); 29 | if (is TimeZone converter) { 30 | value messageSource = messageSourceFromRequest(request, services, defaultServices.messageSource); 31 | if (is MessageSource messageSource) { 32 | value eventProducer = eventProducerFromRequest(request, services, defaultServices.eventProducer); 33 | if (is EventProducer eventProducer) { 34 | value calendar = calendarFromRequest(request, services, defaultServices.calendar); 35 | if (is CalendarService calendar) { 36 | return TimeServices ( 37 | converter, messageSource, eventProducer, calendar 38 | ); 39 | } 40 | else { 41 | return calendar; 42 | } 43 | } 44 | else { 45 | return eventProducer; 46 | } 47 | } 48 | else { 49 | return messageSource; 50 | } 51 | } 52 | else { 53 | return converter; 54 | } 55 | 56 | } 57 | 58 | 59 | "Extracts time zone from timer or scheduler request." 60 | since("0.3.0") by("Lis") 61 | TimeZone|String> timeZoneFromRequest ( 62 | "Timer description to get time zone name." JsonObject request, 63 | "Services Chime provides." ChimeServices services, 64 | "Default time zone applied if no time zone name is given." TimeZone defaultTimeZone 65 | ) { 66 | if (is String providerType = request[Chime.timeZoneProvider.key]) { 67 | return services.createTimeZone(providerType, request); 68 | } 69 | else if (is String timeZone = request[Chime.key.timeZone]) { 70 | return services.createTimeZone(Chime.timeZoneProvider.jvm, request); 71 | } 72 | else { 73 | return defaultTimeZone; 74 | } 75 | } 76 | 77 | 78 | "Extracts message source from timer or scheduler request." 79 | since("0.3.0") by("Lis") 80 | MessageSource|String> messageSourceFromRequest ( 81 | "Timer description to get time zone name." JsonObject request, 82 | "Services Chime provides." ChimeServices services, 83 | "Default time zone applied if no time zone name is given." MessageSource defaultMessageSource 84 | ) { 85 | if (is String providerType = request[Chime.messageSource.key]) { 86 | return services.createMessageSource ( 87 | providerType, 88 | request.getObjectOrNull(Chime.key.messageSourceOptions) else JsonObject{} 89 | ); 90 | } 91 | else { 92 | return defaultMessageSource; 93 | } 94 | } 95 | 96 | 97 | "Extracts Event producer from timer or scheduler request." 98 | since("0.3.0") by("Lis") 99 | EventProducer|String> eventProducerFromRequest ( 100 | "Timer description to get time zone name." JsonObject request, 101 | "Services Chime provides." ChimeServices services, 102 | "Default event producer applied if no one given at timer create request." 103 | EventProducer defaultProducer 104 | ) { 105 | if (is String providerType = request[Chime.eventProducer.key]) { 106 | return services.createEventProducer ( 107 | providerType, 108 | request.getObjectOrNull(Chime.key.eventProducerOptions) else JsonObject{} 109 | ); 110 | } 111 | else { 112 | if (exists opts = request.getObjectOrNull(Chime.key.eventProducerOptions)) { 113 | return services.createEventProducer(Chime.eventProducer.eventBus, opts); 114 | } 115 | else { 116 | return defaultProducer; 117 | } 118 | } 119 | } 120 | 121 | 122 | "Extracts Calendar from timer or scheduler request." 123 | since("0.3.0") by("Lis") 124 | CalendarService|String> calendarFromRequest ( 125 | "Timer description to get time zone name." JsonObject request, 126 | "Services Chime provides." ChimeServices services, 127 | "Default calendar applied if no one given at timer create request." 128 | CalendarService defaultCalendar 129 | ) { 130 | if (is JsonObject calendarRequest = request[Chime.calendar.key]) { 131 | value cl = services.createCalendar(calendarRequest); 132 | if (is Calendar cl) { 133 | return CalendarServiceImpl ( 134 | if (is Boolean ignore = calendarRequest[Chime.calendar.ignoreEvent]) then ignore else true, 135 | cl 136 | ); 137 | } 138 | else { 139 | return cl; 140 | } 141 | } 142 | else { 143 | return defaultCalendar; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/package.ceylon: -------------------------------------------------------------------------------- 1 | "Chime." 2 | since( "0.1.0" ) by( "Lis" ) 3 | shared package herd.schedule.chime; 4 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/schedulerTopLevel.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core.eventbus { 2 | EventBus, 3 | DeliveryOptions 4 | } 5 | import ceylon.json { 6 | JsonObject, 7 | JsonArray, 8 | ObjectValue 9 | } 10 | 11 | 12 | "Connects to already existed scheduler." 13 | see(`interface Scheduler`) 14 | throws(`class AssertionError`, "scheduler name contains ':'") 15 | tagged("Proxy") 16 | since("0.2.0") by("Lis") 17 | shared void connectScheduler ( 18 | "Handler to receive created scheduler or error if occured." 19 | Anything(Throwable|Scheduler) handler, 20 | "Address to call _Chime_." String shimeAddress, 21 | "Event bus to send message to _Chime_." EventBus eventBus, 22 | "Name of the scheduler to be connected or created. 23 | Must not contain ':' symbol, since it separates scheduler and timer names." 24 | String name, 25 | "Timeout to send message with." 26 | Integer? sendTimeout = null 27 | ) { 28 | "Scheduler name must not contain ``Chime.configuration.nameSeparator``, since it separates scheduler and timer names." 29 | assert(!name.contains(Chime.configuration.nameSeparator)); 30 | JsonObject request = JsonObject { 31 | Chime.key.operation -> Chime.operation.info, 32 | Chime.key.name -> name 33 | }; 34 | if (exists sendTimeout) { 35 | eventBus.send ( 36 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout), 37 | SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout) 38 | ); 39 | } 40 | else { 41 | eventBus.send ( 42 | shimeAddress, request, 43 | SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout) 44 | ); 45 | } 46 | 47 | } 48 | 49 | 50 | "Creates new scheduler or connects to already existed scheduler." 51 | see(`interface Scheduler`) 52 | throws(`class AssertionError`, "scheduler name contains ':'") 53 | tagged("Proxy") 54 | since("0.2.0") by("Lis") 55 | shared void createScheduler ( 56 | "Handler to receive created scheduler or error if occured." 57 | Anything(Throwable|Scheduler) handler, 58 | "Address to call _Chime_." String shimeAddress, 59 | "Event bus to send message to _Chime_." EventBus eventBus, 60 | "Name of the scheduler to be connected or created. 61 | Must not contain ':' symbol, since it separates scheduler and timer names." 62 | String name, 63 | "`True` if new scheduler is paused and `false` if running. 64 | If scheduler has been created early its state is not changed." 65 | Boolean paused = false, 66 | "Optional time zone default for the scheduler." 67 | String? timeZone = null, 68 | "Optional time zone provider, default is \"jvm\"." 69 | String? timeZoneProvider = null, 70 | "Optional message source type default for the scheduler." 71 | String? messageSource = null, 72 | "Optional configuration passed to message source factory." 73 | ObjectValue? messageSourceConfig = null, 74 | "Event producer provider." 75 | String? eventProducer = null, 76 | "Optional configuration passed to event producer factory." 77 | JsonObject? eventProducerOptions = null, 78 | "Timeout to send message with." 79 | Integer? sendTimeout = null 80 | ) { 81 | "Scheduler name must not contain ``Chime.configuration.nameSeparator``, since it separates scheduler and timer names." 82 | assert(!name.contains(Chime.configuration.nameSeparator)); 83 | JsonObject request = JsonObject { 84 | Chime.key.operation -> Chime.operation.create, 85 | Chime.key.name -> name, 86 | Chime.key.state -> (if (paused) then Chime.state.paused else Chime.state.running) 87 | }; 88 | if (exists timeZone) { 89 | request.put(Chime.key.timeZone, timeZone); 90 | if (exists timeZoneProvider) { 91 | request.put( Chime.key.timeZoneProvider, timeZoneProvider ); 92 | } 93 | } 94 | if (exists messageSource) { 95 | request.put(Chime.key.messageSource, messageSource); 96 | if (exists messageSourceConfig) { 97 | request.put(Chime.key.messageSourceOptions, messageSourceConfig); 98 | } 99 | } 100 | if (exists eventProducer) { 101 | request.put(Chime.key.eventProducer, eventProducer); 102 | } 103 | if (exists eventProducerOptions) { 104 | request.put(Chime.key.eventProducerOptions, eventProducerOptions); 105 | } 106 | 107 | if (exists sendTimeout) { 108 | eventBus.send ( 109 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout), 110 | SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout) 111 | ); 112 | } 113 | else { 114 | eventBus.send ( 115 | shimeAddress, request, SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout) 116 | ); 117 | } 118 | } 119 | 120 | 121 | "Returns info on the given schedulers." 122 | tagged("Proxy") 123 | see(`function Scheduler.timersInfo`, `function Scheduler.info`) 124 | since("0.2.0") by("Lis") 125 | shared void schedulerInfo ( 126 | "Handler to receive scheduler infos or error if occured." 127 | Anything(Throwable|SchedulerInfo[]) handler, 128 | "Address to call _Chime_." 129 | String shimeAddress, 130 | "Event bus to send message to _Chime_." 131 | EventBus eventBus, 132 | "List of scheduler name, the info to be requested for. 133 | If empty then info on all schedulers are requested." 134 | {String*} names = {}, 135 | "Timeout to send message with." 136 | Integer? sendTimeout = null 137 | 138 | ) { 139 | JsonObject request = JsonObject { 140 | Chime.key.operation -> Chime.operation.info 141 | }; 142 | if (!names.empty) { 143 | request.put(Chime.key.name, JsonArray(names)); 144 | } 145 | 146 | if ( exists sendTimeout ) { 147 | eventBus.send ( 148 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout), 149 | SchedulerImpl.replyWithInfo(handler) 150 | ); 151 | } 152 | else { 153 | eventBus.send ( 154 | shimeAddress, request, SchedulerImpl.replyWithInfo(handler) 155 | ); 156 | } 157 | } 158 | 159 | 160 | "Deletes schedulers or timers with the given names." 161 | tagged("Proxy") 162 | see(`function Scheduler.deleteTimers`, `function Scheduler.delete`) 163 | since("0.2.1") by("Lis") 164 | shared void delete ( 165 | "Address to call _Chime_." 166 | String shimeAddress, 167 | "Event bus to send message to _Chime_." 168 | EventBus eventBus, 169 | "List of scheduler or timer names. 170 | If empty then every scheduler and timer have to be deleted." 171 | {String*} names = {}, 172 | "Optional handler called with a list of names of actually deleted schedulers or timers." 173 | Anything(Throwable|{String*})? handler = null, 174 | "Timeout to send message with." 175 | Integer? sendTimeout = null 176 | ) { 177 | JsonObject request = JsonObject { 178 | Chime.key.operation -> Chime.operation.delete, 179 | Chime.key.name -> (if (names.empty) then "" else JsonArray(names)) 180 | }; 181 | if (exists handler) { 182 | if (exists sendTimeout) { 183 | eventBus.send ( 184 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout), 185 | SchedulerImpl.replyWithList(handler, Chime.key.schedulers) 186 | ); 187 | } 188 | else { 189 | eventBus.send ( 190 | shimeAddress, request, SchedulerImpl.replyWithList(handler, Chime.key.schedulers) 191 | ); 192 | } 193 | } 194 | else { 195 | if (exists sendTimeout) { 196 | eventBus.send(shimeAddress, request, DeliveryOptions(null, null, sendTimeout)); 197 | } 198 | else { 199 | eventBus.send(shimeAddress, request); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/ChimeServices.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | JsonObject 3 | } 4 | import io.vertx.ceylon.core { 5 | 6 | Vertx 7 | } 8 | import herd.schedule.chime.service.timer { 9 | TimeRow, 10 | TimeRowFactory 11 | } 12 | import herd.schedule.chime.service.timezone { 13 | TimeZone 14 | } 15 | import herd.schedule.chime.service.message { 16 | MessageSource 17 | } 18 | import herd.schedule.chime.service.producer { 19 | EventProducer 20 | } 21 | import herd.schedule.chime { 22 | Chime 23 | } 24 | import herd.schedule.chime.service.calendar { 25 | Calendar 26 | } 27 | 28 | 29 | "Provides Chime services: 30 | * creating [[TimeRow]] by the given timer description 31 | * creating [[TimeZone]] with the given provider and the given time zone 32 | * creating [[MessageSource]] with the given provider and the given details of the source 33 | " 34 | see(`interface TimeRowFactory`) 35 | since("0.3.0") by("Lis") 36 | shared interface ChimeServices 37 | { 38 | 39 | shared formal Service|String> createService ( 40 | "Type of the service provider." String providerType, 41 | "Options passed to service provider." JsonObject options 42 | ); 43 | 44 | "Creates time row by timer description. See about description in [[module herd.schedule.chime]]." 45 | shared default TimeRow|String> createTimeRow("Timer description." JsonObject description) { 46 | if (is String type = description[Chime.key.type]) { 47 | return createService(type, description); 48 | } 49 | else { 50 | return Chime.errors.codeTimerTypeHasToBeSpecified->Chime.errors.timerTypeHasToBeSpecified; 51 | } 52 | } 53 | 54 | "Creates calendar by description. See about description in [[module herd.schedule.chime]]." 55 | shared default Calendar|String> createCalendar("Calendar description." JsonObject description) { 56 | if (is String type = description[Chime.calendar.type]) { 57 | return createService(type, description); 58 | } 59 | else { 60 | return Chime.errors.codeCalendarTypeHasToBeSpecified->Chime.errors.calendarTypeHasToBeSpecified; 61 | } 62 | } 63 | 64 | "Creates time zone with given provider and for the given time zone name." 65 | shared default TimeZone|String> createTimeZone ( 66 | "Type of the time zone provider." String providerType, 67 | "Time zone options." JsonObject options 68 | ) => createService(providerType, options); 69 | 70 | "Creates new message source." 71 | shared default MessageSource|String> createMessageSource ( 72 | "Type of the message source provider." String providerType, 73 | "Message source options." JsonObject options 74 | ) => createService(providerType, options); 75 | 76 | "Creates new event producer." 77 | shared default EventProducer|String> createEventProducer ( 78 | "Type of the event producer provider." String providerType, 79 | "Event producer options." JsonObject options 80 | ) => createService(providerType, options); 81 | 82 | "Time zone local to running machine." 83 | shared formal TimeZone localTimeZone; 84 | 85 | "Vertx instance the _Chime_ is running within." 86 | shared formal Vertx vertx; 87 | 88 | "Event bus address the _Chime_ listens to." 89 | shared formal String address; 90 | } 91 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/Extension.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core { 2 | Vertx 3 | } 4 | import ceylon.json { 5 | 6 | JsonObject 7 | } 8 | import ceylon.language.meta.model { 9 | Type 10 | } 11 | 12 | 13 | "Extension (given as service provider) which acts as factory for some _Chime_ structural elements." 14 | since("0.3.0") by("Lis") 15 | shared interface Extension 16 | { 17 | "Type of service the extension provides." 18 | shared formal String type; 19 | 20 | "Type parameter the extension provides." 21 | shared Type parameter => `Element`; 22 | 23 | "Initializes the extension. 24 | Has to call `complete` when initialization is completed. 25 | By default immediately calls `complete`." 26 | shared default void initialize ( 27 | "Vertx instance the _Chime_ is starting within." 28 | Vertx vertx, 29 | "Configuration the _Chime_ is starting with." 30 | JsonObject config, 31 | "Handler which has to be called when the extension initialization is completed. 32 | The handler takes extension to be added to the _Chime_ 33 | or an error occured during initialization." 34 | Anything(Extension|Throwable) complete 35 | ) => complete(this); 36 | 37 | "Creates new structural element." 38 | shared formal Element|String> create ( 39 | "Provides Chime services." ChimeServices services, 40 | "Options applied to the factory." JsonObject options 41 | ); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/Annually.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime, 3 | dateTime 4 | } 5 | import ceylon.time.base { 6 | Month, 7 | monthOf 8 | } 9 | import herd.schedule.chime.service { 10 | Extension, 11 | ChimeServices 12 | } 13 | import ceylon.json { 14 | JsonObject, 15 | JsonArray 16 | } 17 | import herd.schedule.chime { 18 | Chime 19 | } 20 | import herd.schedule.chime.cron { 21 | calendar 22 | } 23 | 24 | 25 | "Restricts time to the given months." 26 | since("0.3.0") by("Lis") 27 | class Annually(Set months) satisfies Calendar 28 | { 29 | shared actual Boolean inside(DateTime date) => date.month in months; 30 | 31 | shared actual DateTime nextOutside(DateTime date) { 32 | if (inside(date)) { 33 | variable DateTime ret = date.plusMonths(1); 34 | ret = dateTime(ret.year, ret.month, 1, ret.hours, ret.minutes, ret.seconds, 0); 35 | while (inside(ret)) { 36 | ret = ret.plusMonths(1); 37 | } 38 | return ret; 39 | } 40 | else { 41 | return date; 42 | } 43 | } 44 | } 45 | 46 | 47 | "Service provider of annually calendar. 48 | I.e. calendar which excludes a given list of months. 49 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 50 | \"calendar\" -> JsonObject { 51 | \"type\" -> \"annually\"; 52 | // monthes to be excluded from the fire event 53 | // digital, full or short (3 letters) string names are admitted 54 | \"months\" -> JsonArray{1, February, Mar} 55 | }; 56 | " 57 | service(`interface Extension`) 58 | since("0.3.0") by("Lis") 59 | shared class AnnuallyFactory() satisfies CalendarFactory 60 | { 61 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 62 | if (is JsonArray months = options[Chime.date.months]) { 63 | try { 64 | return Annually(set{for (item in months.narrow()) monthOf(calendar.digitalMonth(item))}); 65 | } 66 | catch (AssertionError err) { 67 | return Chime.errors.assertionErrorCode -> err.message; 68 | } 69 | } 70 | else { 71 | return Chime.errors.codeAnnuallyCalendarMonths-> Chime.errors.annuallyCalendarMonths; 72 | } 73 | } 74 | 75 | shared actual String type => Chime.calendar.annually; 76 | 77 | } 78 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/Calendar.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | 5 | 6 | "Restricts timer fire date - time." 7 | since("0.3.0") by("Lis") 8 | shared interface Calendar 9 | { 10 | "`true` if given date - time is inside the calendar and `false` if outside." 11 | shared formal Boolean inside(DateTime date); 12 | 13 | "Returns next date - time outside the calendar." 14 | shared formal DateTime nextOutside(DateTime date); 15 | } 16 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/CalendarFactory.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | ChimeServices, 3 | Extension 4 | } 5 | import ceylon.json { 6 | JsonObject 7 | } 8 | 9 | 10 | "Calendar provider - creates [[Calendar]]." 11 | since("0.3.0") by("Lis") 12 | shared interface CalendarFactory satisfies Extension 13 | { 14 | "Creates new calendar with the given description. 15 | Returns created [[Calendar]] or error code -> message pair if some error occured." 16 | shared actual formal Calendar|String> create ( 17 | "Provides Chime services." ChimeServices services, 18 | "Options with \"calendar\" description." JsonObject options 19 | ); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/Daily.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | Time, 3 | Period, 4 | DateTime, 5 | time 6 | } 7 | import herd.schedule.chime.service { 8 | Extension, 9 | ChimeServices 10 | } 11 | import ceylon.json { 12 | JsonObject 13 | } 14 | import herd.schedule.chime { 15 | Chime 16 | } 17 | 18 | 19 | "Resticts timer fire to to the given time range." 20 | since("0.3.0") by("Lis") 21 | class Daily(Time from, Time to, Period tolerancePeriod) satisfies Calendar 22 | { 23 | Time shifted = to.plus(tolerancePeriod); 24 | 25 | shared actual Boolean inside(DateTime date) { 26 | value t = date.time; 27 | return t >= from && t <= to; 28 | } 29 | 30 | shared actual DateTime nextOutside(DateTime date) { 31 | if (inside(date)) { 32 | return date.date.at(shifted); 33 | } 34 | else { 35 | return date; 36 | } 37 | } 38 | } 39 | 40 | 41 | "Service provider of daily calendar. 42 | I.e. calendar which excludes _from - to_ time of day. 43 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 44 | \"calendar\" -> JsonObject { 45 | \"type\" -> \"daily\"; 46 | // start time to be excluded 47 | \"from\" -> JsonObject { 48 | \"hours\" -> XXX; // default is 0 49 | \"minutes\" -> XXX; // default is 0 50 | \"seconds\" -> XXX; // default is 0 51 | }; 52 | // end time to be excluded 53 | \"to\" -> JsonObject { 54 | \"hours\" -> XXX; // default is 0 55 | \"minutes\" -> XXX; // default is 0 56 | \"seconds\" -> XXX; // default is 0 57 | }; 58 | // seconds to be added to the 'to' time when next out of calendar date is searched, default is 1s 59 | \"tolerance\" -> XXX; 60 | }; 61 | " 62 | service(`interface Extension`) 63 | since("0.3.0") by("Lis") 64 | shared class DailyFactory() satisfies CalendarFactory 65 | { 66 | 67 | Time timeFromJson(JsonObject descr) 68 | => time ( 69 | if (is Integer t = descr[Chime.date.hours]) then t else 0, 70 | if (is Integer t = descr[Chime.date.minutes]) then t else 0, 71 | if (is Integer t = descr[Chime.date.seconds]) then t else 0 72 | ); 73 | 74 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 75 | if (is JsonObject fromDescr = options[Chime.calendar.from], 76 | is JsonObject toDescr = options[Chime.calendar.to] 77 | ) { 78 | Time from = timeFromJson(fromDescr); 79 | Time to = timeFromJson(toDescr); 80 | if (to > from) { 81 | return Daily ( 82 | from, to, 83 | Period{seconds = if (is Integer t = options[Chime.calendar.tolerance]) then t else 1;} 84 | ); 85 | } 86 | else { 87 | return Chime.errors.codeDailyCalendarOrder -> Chime.errors.dailyCalendarOrder; 88 | } 89 | } 90 | else { 91 | return Chime.errors.codeDailyCalendarFormat -> Chime.errors.dailyCalendarFormat; 92 | } 93 | } 94 | 95 | shared actual String type => Chime.calendar.daily; 96 | 97 | } 98 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/Dativity.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import herd.schedule.chime.service { 5 | ChimeServices, 6 | Extension 7 | } 8 | import ceylon.json { 9 | JsonObject, 10 | JsonArray 11 | } 12 | import herd.schedule.chime { 13 | Chime 14 | } 15 | 16 | 17 | "Restricts time to the given dates." 18 | since("0.3.0") by("Lis") 19 | class Dativity(Set dates) satisfies Calendar 20 | { 21 | shared actual Boolean inside(DateTime date) => DayMonth.fromDate(date.date) in dates; 22 | 23 | shared actual DateTime nextOutside(DateTime date) { 24 | variable DateTime ret = date; 25 | while (inside(ret)) { 26 | ret = ret.plusDays(1); 27 | } 28 | return ret; 29 | } 30 | } 31 | 32 | 33 | "Service provider of dativity calendar. 34 | I.e. calendar which excludes a given list of dates. 35 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 36 | \"calendar\" -> JsonObject { 37 | \"type\" -> \"dativity\"; 38 | // List of dates to be excluded from the fire event. 39 | // Each date is JsonObject which contains day, month and optionally year. 40 | // If year is omitted the date is applied to any year 41 | // If only a one date is given it can be specified without JsonArray, i.e. JsonObject can be stored under dates key 42 | \"dates\" -> JsonArray { 43 | JsonObject { 44 | \"day of month\" -> Integer day 45 | \"month\" -> Integer or String month 46 | \"year\" -> Integer year, optional 47 | } 48 | } 49 | }; 50 | " 51 | service(`interface Extension`) 52 | since("0.3.0") by("Lis") 53 | shared class DativityFactory() satisfies CalendarFactory 54 | { 55 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 56 | value dativity = options[Chime.calendar.dates]; 57 | if (is JsonArray dativity) { 58 | try { 59 | return Dativity(set{for(item in dativity.narrow()) DayMonth.fromJson(item)}); 60 | 61 | } 62 | catch (AssertionError err) { 63 | return Chime.errors.assertionErrorCode -> err.message; 64 | } 65 | } 66 | else if (is JsonObject dativity) { 67 | try { 68 | return Dativity(set{DayMonth.fromJson(dativity)}); 69 | 70 | } 71 | catch (AssertionError err) { 72 | return Chime.errors.assertionErrorCode -> err.message; 73 | } 74 | } 75 | else { 76 | return Chime.errors.codeDativityCalendarDates-> Chime.errors.dativityCalendarDates; 77 | } 78 | } 79 | 80 | shared actual String type => Chime.calendar.dativity; 81 | 82 | } 83 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/DayMonth.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time.base { 2 | Month, 3 | monthOf 4 | } 5 | import ceylon.time { 6 | Date 7 | } 8 | import ceylon.json { 9 | JsonObject 10 | } 11 | import herd.schedule.chime { 12 | Chime 13 | } 14 | import herd.schedule.chime.cron { 15 | calendar 16 | } 17 | 18 | 19 | "Contains day, month and optional year." 20 | since("0.3.0") by("Lis") 21 | class DayMonth extends Object { 22 | 23 | shared static DayMonth fromJson(JsonObject descr) { 24 | "Year has to be Integer or undefined." 25 | assert (is Integer? y = descr[Chime.date.year]); 26 | return DayMonth.with(descr.getInteger(Chime.date.dayOfMonth), calendar.monthFromJson(descr, Chime.date.month), y); 27 | } 28 | 29 | 30 | Integer day; 31 | Month month; 32 | Integer? year; 33 | 34 | shared new fromDate(Date date) extends Object() { 35 | this.day = date.day; 36 | this.month = date.month; 37 | this.year = date.year; 38 | } 39 | 40 | shared new with(Integer day, Month|Integer month, Integer? year = null) extends Object() { 41 | this.day = day; 42 | this.month = monthOf(month); 43 | this.year = year; 44 | } 45 | 46 | shared actual Boolean equals(Object obj) { 47 | if (is DayMonth obj) { 48 | if (day == obj.day && month == obj.month) { 49 | if (exists y = year, exists objy = obj.year) { 50 | return y == objy; 51 | } 52 | else { 53 | return true; 54 | } 55 | } 56 | else { 57 | return false; 58 | } 59 | } 60 | else { 61 | return false; 62 | } 63 | } 64 | 65 | shared actual Integer hash { 66 | variable value hash = 1; 67 | hash = 31*hash + day; 68 | hash = 31*hash + month.hash; 69 | return hash; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/IntersectionCalendar.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import herd.schedule.chime.service { 5 | ChimeServices, 6 | Extension 7 | } 8 | import ceylon.json { 9 | JsonObject 10 | } 11 | import herd.schedule.chime { 12 | Chime 13 | } 14 | 15 | 16 | "Intersects calendars with logical `and`." 17 | since("0.3.0") by("Lis") 18 | class IntersectionCalendar({Calendar*} calendars) satisfies Calendar 19 | { 20 | shared actual Boolean inside(DateTime date) { 21 | for (item in calendars) { 22 | if (!item.inside(date)) { 23 | return false; 24 | } 25 | } 26 | return true; 27 | } 28 | 29 | shared actual DateTime nextOutside(DateTime date) { 30 | variable DateTime ret = date; 31 | while (inside(ret)) { 32 | variable DateTime? min = null; 33 | for (item in calendars) { 34 | if (item.inside(ret)) { 35 | value f = item.nextOutside(ret); 36 | if (exists ff = min) { 37 | if (f < ff) {min = f;} 38 | } 39 | else {min = f;} 40 | } 41 | } 42 | if (exists m = min) {ret = m;} 43 | else {break;} 44 | } 45 | return ret; 46 | } 47 | 48 | } 49 | 50 | 51 | "Service provider of intersection calendar. 52 | I.e. calendar which intersects a given list of calendars. 53 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 54 | \"calendar\" -> JsonObject { 55 | \"type\" -> \"intersection\"; 56 | // List of calendars to be intersected. 57 | \"calendars\" -> JsonArray { 58 | // calendars in `JsonObject`'s 59 | } 60 | }; 61 | " 62 | service(`interface Extension`) 63 | since("0.3.0") by("Lis") 64 | shared class IntersectionFactory() satisfies CalendarFactory 65 | { 66 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 67 | value calendars = extractCalendars(services, options); 68 | if (is {Calendar*} calendars) { 69 | return IntersectionCalendar(calendars); 70 | } 71 | else { 72 | return calendars; 73 | } 74 | } 75 | 76 | shared actual String type => Chime.calendar.intersection; 77 | 78 | } -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/LastDayOfWeek.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time.base { 2 | DayOfWeek 3 | } 4 | import ceylon.time { 5 | DateTime 6 | } 7 | import herd.schedule.chime.service { 8 | Extension, 9 | ChimeServices 10 | } 11 | import ceylon.json { 12 | JsonObject 13 | } 14 | import herd.schedule.chime { 15 | Chime 16 | } 17 | import herd.schedule.chime.cron { 18 | calendar 19 | } 20 | 21 | 22 | "Restricts time to the last day of week of the month." 23 | since("0.3.0") by("Lis") 24 | class LastDayOfWeek(DayOfWeek dw) satisfies Calendar 25 | { 26 | shared actual Boolean inside(DateTime date) => date.dayOfWeek == dw && date.plusWeeks(1).month != date.month; 27 | 28 | shared actual DateTime nextOutside(DateTime date) { 29 | if (inside(date)) { 30 | return date.plusDays(1); 31 | } 32 | else { 33 | return date; 34 | } 35 | } 36 | } 37 | 38 | 39 | "Service provider of last day of week calendar. 40 | I.e. calendar which excludes only a last day of week in each month. 41 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 42 | \"calendar\" -> JsonObject { 43 | \"type\" -> \"last day of week\"; 44 | \"day of week\" -> Integer|String // day of week number or name (Sunday has number 1) 45 | }; 46 | " 47 | service(`interface Extension`) 48 | since("0.3.0") by("Lis") 49 | shared class LastDayOfWeekFactory() satisfies CalendarFactory 50 | { 51 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 52 | try { 53 | return LastDayOfWeek(calendar.dayOfWeekFromJson(options, Chime.date.dayOfWeek)); 54 | } 55 | catch (AssertionError err) { 56 | return Chime.errors.assertionErrorCode -> err.message; 57 | } 58 | } 59 | 60 | shared actual String type => Chime.calendar.lastDayOfWeek; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/Monthly.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import herd.schedule.chime.service { 5 | ChimeServices, 6 | Extension 7 | } 8 | import ceylon.json { 9 | JsonObject, 10 | JsonArray 11 | } 12 | import herd.schedule.chime { 13 | Chime 14 | } 15 | 16 | 17 | "Retricts time to the given days of month." 18 | since("0.3.0") by("Lis") 19 | class Monthly(Set restrictedDays) satisfies Calendar 20 | { 21 | shared actual Boolean inside(DateTime date) => date.day in restrictedDays; 22 | 23 | shared actual DateTime nextOutside(DateTime date) { 24 | variable DateTime ret = date; 25 | while (inside(ret)) { 26 | ret = ret.plusDays(1); 27 | } 28 | return ret; 29 | } 30 | } 31 | 32 | "Service provider of monthly calendar. 33 | I.e. calendar which excludes a given list of days for every month. 34 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 35 | \"calendar\" -> JsonObject { 36 | \"type\" -> \"monthly\"; 37 | \"days of month\" -> JsonArray{1, 2, 3} // days to be excluded from the fire event 38 | }; 39 | " 40 | service(`interface Extension`) 41 | since("0.3.0") by("Lis") 42 | shared class MonthlyFactory() satisfies CalendarFactory 43 | { 44 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 45 | if (is JsonArray days = options[Chime.date.daysOfMonth]) { 46 | return Monthly(set(days.narrow())); 47 | } 48 | else { 49 | return Chime.errors.codeMonthlyCalendarDaysOfMonth-> Chime.errors.monthlyCalendarDaysOfMonth; 50 | } 51 | } 52 | 53 | shared actual String type => Chime.calendar.monthly; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/UnionCalendar.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import herd.schedule.chime.service { 5 | ChimeServices, 6 | Extension 7 | } 8 | import ceylon.json { 9 | JsonObject 10 | } 11 | import herd.schedule.chime { 12 | Chime 13 | } 14 | 15 | 16 | "Unions calendars with logical `or`." 17 | since("0.3.0") by("Lis") 18 | class UnionCalendar({Calendar*} calendars) satisfies Calendar 19 | { 20 | shared actual Boolean inside(DateTime date) { 21 | for (item in calendars) { 22 | if (item.inside(date)) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | shared actual DateTime nextOutside(DateTime date) { 30 | variable DateTime ret = date; 31 | while (inside(ret)) { 32 | variable DateTime max = ret; 33 | for (item in calendars) { 34 | if (item.inside(ret)) { 35 | value f = item.nextOutside(ret); 36 | if (f > max) {max = f;} 37 | } 38 | } 39 | ret = max; 40 | } 41 | return ret; 42 | } 43 | 44 | } 45 | 46 | 47 | "Service provider of union calendar. 48 | I.e. calendar which unons a given list of calendar. 49 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 50 | \"calendar\" -> JsonObject { 51 | \"type\" -> \"union\"; 52 | // List of calendars to be unioned. 53 | \"calendars\" -> JsonArray { 54 | // calendars in `JsonObject`'s 55 | } 56 | }; 57 | " 58 | service(`interface Extension`) 59 | since("0.3.0") by("Lis") 60 | shared class UnionFactory() satisfies CalendarFactory 61 | { 62 | 63 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 64 | value calendars = extractCalendars(services, options); 65 | if (is {Calendar*} calendars) { 66 | return UnionCalendar(calendars); 67 | } 68 | else { 69 | return calendars; 70 | } 71 | } 72 | 73 | shared actual String type => Chime.calendar.union; 74 | 75 | } -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/WeekdayOfMonth.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time.base { 2 | DayOfWeek 3 | } 4 | import ceylon.time { 5 | DateTime 6 | } 7 | import herd.schedule.chime.service { 8 | ChimeServices, 9 | Extension 10 | } 11 | import herd.schedule.chime.cron { 12 | calendar 13 | } 14 | import ceylon.json { 15 | JsonObject 16 | } 17 | import herd.schedule.chime { 18 | Chime 19 | } 20 | 21 | 22 | "Restricts time to the given weekday of month." 23 | since("0.3.0") by("Lis") 24 | class WeekdayOfMonth(DayOfWeek dw, Integer order) satisfies Calendar 25 | { 26 | shared actual Boolean inside(DateTime date) => date.dayOfWeek == dw && order == (date.day - 1) / 7 + 1; 27 | 28 | shared actual DateTime nextOutside(DateTime date) { 29 | if (inside(date)) { 30 | return date.plusDays(1); 31 | } 32 | else { 33 | return date; 34 | } 35 | } 36 | 37 | } 38 | 39 | 40 | "Service provider of weekday of month calendar. 41 | I.e. calendar which excludes a given weekday with the given order in each month (i.e. excludes nth weekday). 42 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 43 | \"calendar\" -> JsonObject { 44 | \"type\" -> \"weekday of month\"; 45 | // day of week number or name (Sunday has number 1), 46 | // digital, full or short (3 letters) string names are admitted 47 | \"day of week\" -> Integer|String; 48 | \"order\" -> Integer // weekday order 49 | }; 50 | " 51 | service(`interface Extension`) 52 | since("0.3.0") by("Lis") 53 | shared class WeekdayOfMonthFactory() satisfies CalendarFactory 54 | { 55 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 56 | try { 57 | return WeekdayOfMonth ( 58 | calendar.dayOfWeekFromJson(options, Chime.date.dayOfWeek), 59 | options.getInteger(Chime.date.order) 60 | ); 61 | } 62 | catch (AssertionError err) { 63 | return Chime.errors.assertionErrorCode -> err.message; 64 | } 65 | } 66 | 67 | shared actual String type => Chime.calendar.weekdayOfMonth; 68 | 69 | } 70 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/Weekly.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import ceylon.time.base { 5 | DayOfWeek 6 | } 7 | import herd.schedule.chime.service { 8 | ChimeServices, 9 | Extension 10 | } 11 | import herd.schedule.chime.cron { 12 | calendar 13 | } 14 | import ceylon.json { 15 | JsonObject, 16 | JsonArray 17 | } 18 | import herd.schedule.chime { 19 | Chime 20 | } 21 | 22 | 23 | "Restricts time to week days." 24 | since("0.3.0") by("Lis") 25 | class Weekly(Set restrictedDays) satisfies Calendar 26 | { 27 | shared actual Boolean inside(DateTime date) => date.dayOfWeek in restrictedDays; 28 | 29 | shared actual DateTime nextOutside(DateTime date) { 30 | variable DateTime ret = date; 31 | while (inside(ret)) { 32 | ret = ret.plusDays(1); 33 | } 34 | return ret; 35 | } 36 | } 37 | 38 | 39 | "Service provider of weekly calendar. 40 | I.e. calendar which excludes a given list of week days every week. 41 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`: 42 | \"calendar\" -> JsonObject { 43 | \"type\" -> \"weekly\"; 44 | // days of week to be excluded from the fire event, Sunday has index 1 45 | // digital, full or short (3 letters) string names are admitted 46 | \"days of week\" -> JsonArray{1, Monday, Tue} 47 | }; 48 | " 49 | service(`interface Extension`) 50 | since("0.3.0") by("Lis") 51 | shared class WeeklyFactory() satisfies CalendarFactory 52 | { 53 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) { 54 | if (is JsonArray dow = options[Chime.date.daysOfWeek]) { 55 | try { 56 | return Weekly(set{for (item in dow.narrow()) calendar.dayOfWeekFromString(item)}); 57 | } 58 | catch (AssertionError err) { 59 | return Chime.errors.assertionErrorCode -> err.message; 60 | } 61 | } 62 | else { 63 | return Chime.errors.codeWeeklyCalendarDaysOfWeek-> Chime.errors.weeklyCalendarDaysOfWeek; 64 | } 65 | } 66 | 67 | shared actual String type => Chime.calendar.weekly; 68 | 69 | } 70 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/extractCalendars.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | ChimeServices 3 | } 4 | import ceylon.json { 5 | JsonObject, 6 | JsonArray 7 | } 8 | import ceylon.collection { 9 | ArrayList 10 | } 11 | import herd.schedule.chime { 12 | Chime 13 | } 14 | 15 | 16 | "Extracts a list of calendars from Json description." 17 | since("0.3.0") by("Lis") 18 | {Calendar*}|String> extractCalendars(ChimeServices services, JsonObject options) { 19 | if (is JsonArray calendarDescrs = options[Chime.calendar.calendars]) { 20 | ArrayList calendars = ArrayList(); 21 | for(item in calendarDescrs.narrow()) { 22 | value c = services.createCalendar(item); 23 | if (is Calendar c) { 24 | calendars.add(c); 25 | } 26 | else { 27 | return c; 28 | } 29 | } 30 | return calendars; 31 | } 32 | else { 33 | return Chime.errors.codeCalendars-> Chime.errors.calendars; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/calendar/package.ceylon: -------------------------------------------------------------------------------- 1 | "Provides calendar services. 2 | 3 | Calendar restricts date/time a timer fires at. 4 | So, when calendar is added to timer, 5 | timer will not fire at dates/time calendar specifies. 6 | In order to add calendar to the timer put calendar description to timer 7 | create request under \"calendar\" key. See desciption details at each given calendar. 8 | 9 | If calendar is applied at scheduler level then each timer within the scheduler 10 | with unspecified calendar will get the cheduler level calendar. 11 | 12 | To build your own [[Calendar]] follow the steps: 13 | 1. Implement [[CalendarFactory]]. 14 | 2. Mark the class from 1. with `service(`\` `interface Extension`\` `)`. 15 | 3. Deploy _Chime_ with configuration provided modules to serach the services: 16 | JsonObject { 17 | \"services\" -> [\"module name/module version\"] 18 | } 19 | 20 | " 21 | since("0.3.0") by("Lis") 22 | shared package herd.schedule.chime.service.calendar; 23 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/message/DirectMessageSourceFactory.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | JsonObject, 3 | ObjectValue 4 | } 5 | import herd.schedule.chime { 6 | 7 | Chime, 8 | TimerFire 9 | } 10 | import herd.schedule.chime.service { 11 | ChimeServices, 12 | Extension 13 | } 14 | 15 | 16 | "Factory to create message source which returns message given in timer create request. 17 | This is default message source factory." 18 | service(`interface Extension`) 19 | since("0.3.0") by("Lis") 20 | shared class DirectMessageSourceFactory satisfies MessageSourceFactory 21 | { 22 | 23 | "Default message source - applies `onMessage` with given event message." 24 | shared static object directMessageSource satisfies MessageSource { 25 | shared actual void extract(TimerFire event, Anything(ObjectValue?) onMessage) 26 | => onMessage(event.message); 27 | } 28 | 29 | "New `DirectMessageSourceFactory` instance." 30 | shared new () {} 31 | 32 | 33 | shared actual MessageSource|String> create(ChimeServices services, JsonObject config) 34 | => directMessageSource; 35 | 36 | shared actual String type => Chime.messageSource.direct; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/message/MessageSource.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime { 2 | TimerFire 3 | } 4 | import ceylon.json { 5 | ObjectValue 6 | } 7 | 8 | 9 | "Extension which provides messages to be added to a timer fire event. 10 | Generally, a message source is instantiated by [[MessageSourceFactory]] given as service provider, 11 | see [[package herd.schedule.chime.service]]. 12 | 13 | It is proposed that message attached to the timer create request contains some info for this source. 14 | The message source may use this info to extract provided message. 15 | " 16 | since("0.3.0") by("Lis") 17 | see(`interface MessageSourceFactory`) 18 | shared interface MessageSource 19 | { 20 | "Extracts message and message headers from this source. 21 | When message and message headers are ready call `onMessage` handler." 22 | shared formal void extract ( 23 | "Event the message to be attached to. 24 | [[TimerFire.message]] is taken from timer or scheduler create request. 25 | In the sent event the message is to be replaced with one the given to `onMessage` handler." 26 | TimerFire event, 27 | "Handler which takes the message." 28 | Anything(ObjectValue?) onMessage 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/message/MessageSourceFactory.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | JsonObject 3 | } 4 | import herd.schedule.chime.service { 5 | Extension, 6 | ChimeServices 7 | } 8 | 9 | 10 | "Creates message source." 11 | since("0.3.0") by("Lis") 12 | shared interface MessageSourceFactory satisfies Extension 13 | { 14 | "Creates new message source." 15 | shared actual formal MessageSource|String> create ( 16 | "Provides Chime services." ChimeServices services, 17 | "Message source configuration came with scheduler or timer create request." JsonObject config 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/message/package.ceylon: -------------------------------------------------------------------------------- 1 | "Provides message source services. 2 | 3 | A timer may fire with a message attached to the fire event. The message and message headers may be extracted from some source. 4 | [[MessageSource]] provides a way to extract messages. A service provider implementing [[MessageSourceFactory]] 5 | is responsible to instantiate particular [[MessageSource]]. 6 | 7 | built-in message sources: 8 | * direct source, which attaches to the fire event a message given at timer create request, 9 | created by [[herd.schedule.chime.service.message::DirectMessageSourceFactory]]. 10 | 11 | To build your own [[herd.schedule.chime.service.message::MessageSource]] follow the steps: 12 | 1. Implement [[herd.schedule.chime.service.message::MessageSourceFactory]]. 13 | 2. Mark the class from 1. with `service(`\` `interface MessageSourceFactory`\` `)`. 14 | 3. Deploy _Chime_ with configuration provided modules to serach the services: 15 | JsonObject { 16 | \"services\" -> [\"module name/module version\"] 17 | } 18 | 19 | In timer or scheduler create request message source can be specified under \"message source\" key. 20 | Additional configuration passed to the factory may be given under \"message source configuration\" key. 21 | 22 | > At scheduler level default message source may be specified, which is applied to timer if no message source 23 | is given at timer create request. 24 | " 25 | since("0.3.0") by("Lis") 26 | shared package herd.schedule.chime.service.message; 27 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/package.ceylon: -------------------------------------------------------------------------------- 1 | "Chime extensions with service providers. 2 | 3 | > See also `ceylon.language.service` annotation. 4 | 5 | Chime searches service providers only in modules specified in verticle deployed configuration: 6 | JsonObject modulesWithProviders = JsonObject { 7 | \"services\" -> [ 8 | \"module 1 name/module 1 version\", 9 | \"module nth name/module nth version\" 10 | ] 11 | }; 12 | Chime().deploy ( 13 | vertx.vertx(), 14 | DeploymentOptions(modulesWithProviders) 15 | ); 16 | 17 | 18 | Each service provider has to satisfy [[Extension]] interface 19 | and be marked with `service(`\` `interface Extension`\` `)` annotation. 20 | 21 | Following services are available: 22 | * timer ([[package herd.schedule.chime.service.timer]]) - instantiating built-in or custom timers 23 | * time zone ([[package herd.schedule.chime.service.timezone]]) - extracting time zone 24 | * message source ([[package herd.schedule.chime.service.message]]) - extracting messages applied to timer fire event 25 | * calendar ([[package herd.schedule.chime.service.calendar]]) - creating calendars which can be applied to bound eventing date/time 26 | * event producer ([[package herd.schedule.chime.service.producer]]) - creating a producer applied to send timer event 27 | 28 | " 29 | since("0.3.0") by("Lis") 30 | shared package herd.schedule.chime.service; 31 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/producer/EBProducerFactory.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | ChimeServices, 3 | Extension 4 | } 5 | import ceylon.json { 6 | JsonObject 7 | } 8 | import herd.schedule.chime { 9 | Chime 10 | } 11 | import io.vertx.ceylon.core.eventbus { 12 | EventBus, 13 | deliveryOptions 14 | } 15 | 16 | 17 | "Factory which creates [[EventBusProducer]]" 18 | service(`interface Extension`) 19 | since("0.3.0") by("Lis") 20 | shared class EBProducerFactory satisfies ProducerFactory 21 | { 22 | 23 | "Creates default producer, which operates via the given event bus and sends events (rather than publishes)." 24 | shared static EventProducer createDefaultProducer(EventBus eventBus) => EventBusProducer(eventBus, false, null); 25 | 26 | "New `ProducerFactory` instance." 27 | shared new () {} 28 | 29 | shared actual EventProducer|String> create(ChimeServices services, JsonObject options) 30 | => EventBusProducer ( 31 | services.vertx.eventBus(), 32 | if (is Boolean b = options[Chime.eventProducer.publish]) then b else false, 33 | if (is JsonObject opts = options[Chime.eventProducer.deliveryOptions]) 34 | then deliveryOptions.fromJson(opts) else null 35 | ); 36 | 37 | shared actual String type => Chime.eventProducer.eventBus; 38 | 39 | shared actual String string => "EventBus producer factory."; 40 | } 41 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/producer/EventBusProducer.ceylon: -------------------------------------------------------------------------------- 1 | import io.vertx.ceylon.core.eventbus { 2 | EventBus, 3 | DeliveryOptions 4 | } 5 | import herd.schedule.chime { 6 | TimerEvent, 7 | TimerCompleted 8 | } 9 | import ceylon.json { 10 | JsonObject 11 | } 12 | 13 | 14 | "Sends timer events via event bus." 15 | since("0.3.0") by("Lis") 16 | class EventBusProducer ( 17 | EventBus eventBus, 18 | Boolean publish, 19 | DeliveryOptions? options 20 | ) 21 | satisfies EventProducer 22 | { 23 | 24 | void publishEvent(TimerEvent event) { 25 | if (exists opt = options) { eventBus.publish(event.timerName, convert(event), opt); } 26 | else { eventBus.publish(event.timerName, convert(event)); } 27 | } 28 | 29 | void sendEvent(TimerEvent event) { 30 | if (exists opt = options) { eventBus.send(event.timerName, convert(event), opt); } 31 | else { eventBus.send(event.timerName, convert(event)); } 32 | } 33 | 34 | "Converts timer event into `JsonObject` which to be send via event bus. 35 | By default applies [[TimerEvent.toJson]]." 36 | shared default JsonObject convert(TimerEvent event) => event.toJson(); 37 | 38 | shared actual void send(TimerEvent event) { 39 | if (publish || event is TimerCompleted) { 40 | publishEvent(event); 41 | } 42 | else { 43 | sendEvent(event); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/producer/EventProducer.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime { 2 | TimerEvent 3 | } 4 | 5 | 6 | "Produces timer events." 7 | since("0.3.0") by("Lis") 8 | shared interface EventProducer 9 | { 10 | "Sends timer event." 11 | shared formal void send(TimerEvent event); 12 | } 13 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/producer/ProducerFactory.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | Extension, 3 | ChimeServices 4 | } 5 | import ceylon.json { 6 | JsonObject 7 | } 8 | 9 | 10 | "Factory to crshared eate [[EventProducer]]." 11 | since("0.3.0") by("Lis") 12 | shared interface ProducerFactory satisfies Extension 13 | { 14 | "Creates new event producer." 15 | shared actual formal EventProducer|String> create ( 16 | "Provides Chime services." ChimeServices services, 17 | "Producer options." JsonObject options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/producer/package.ceylon: -------------------------------------------------------------------------------- 1 | "Extensions which produces timer events." 2 | since("0.3.0") by("Lis") 3 | shared package herd.schedule.chime.service.producer; 4 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/CronFactory.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.cron { 2 | parseCron 3 | } 4 | import ceylon.json { 5 | JsonObject 6 | } 7 | import herd.schedule.chime { 8 | Chime 9 | } 10 | import herd.schedule.chime.service { 11 | ChimeServices, 12 | Extension 13 | } 14 | 15 | 16 | "Factory to create cron-style timers." 17 | service(`interface Extension`) 18 | since("0.3.0") by("Lis") 19 | shared class CronFactory() satisfies TimeRowFactory 20 | { 21 | 22 | Integer maxYearPeriod = 100; 23 | 24 | shared actual String type => Chime.type.cron; 25 | 26 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) { 27 | if (is String seconds = description[Chime.date.seconds], 28 | is String minutes = description[Chime.date.minutes], 29 | is String hours = description[Chime.date.hours], 30 | is String daysOfMonth = description[Chime.date.daysOfMonth], 31 | is String months = description[Chime.date.months] 32 | ) { 33 | // days of week - nonmandatory 34 | String? daysOfWeek; 35 | if (is String str = description[Chime.date.daysOfWeek]) { 36 | daysOfWeek = str; 37 | } 38 | else { 39 | daysOfWeek = null; 40 | } 41 | 42 | // years - nonmandatory 43 | String? years; 44 | if (is String str = description[Chime.date.years]) { 45 | years = str; 46 | } 47 | else { 48 | years = null; 49 | } 50 | 51 | if (exists cronExpr = parseCron(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, years, maxYearPeriod)) { 52 | return TimeRowCronStyle( cronExpr ); 53 | } 54 | else { 55 | return Chime.errors.codeIncorrectCronTimerDescription->Chime.errors.incorrectCronTimerDescription; 56 | } 57 | 58 | } 59 | else { 60 | return Chime.errors.codeIncorrectCronTimerDescription->Chime.errors.incorrectCronTimerDescription; 61 | } 62 | } 63 | 64 | shared actual String string => "cron time row factory"; 65 | 66 | } -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/IntervalFactory.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | JsonObject 3 | } 4 | import herd.schedule.chime { 5 | Chime 6 | } 7 | import herd.schedule.chime.service { 8 | ChimeServices, 9 | Extension 10 | } 11 | 12 | 13 | "Factory to create interval timers." 14 | service(`interface Extension`) 15 | since("0.3.0") by("Lis") 16 | shared class IntervalFactory() satisfies TimeRowFactory 17 | { 18 | 19 | shared actual String type => Chime.type.interval; 20 | 21 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) { 22 | if (is Integer delay = description[Chime.key.delay]) { 23 | if (delay > 0) { 24 | return TimeRowInterval(delay * 1000); 25 | } 26 | else { 27 | return Chime.errors.codeDelayHasToBeGreaterThanZero->Chime.errors.delayHasToBeGreaterThanZero; 28 | } 29 | } 30 | return Chime.errors.codeDelayHasToBeSpecified->Chime.errors.delayHasToBeSpecified; 31 | } 32 | 33 | shared actual String string => "interval time row factory"; 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/TimeRow.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | 3 | DateTime 4 | } 5 | 6 | 7 | "Time row interface. Acts like _enumerator_ but might be restarted from any date. 8 | Generally, a time row is instantiated by [[TimeRowFactory]] given as service provider, 9 | see [[package herd.schedule.chime.service]]." 10 | see(`interface TimeRowFactory`) 11 | since("0.1.0") by("Lis") 12 | shared interface TimeRow 13 | { 14 | 15 | "Starts the timer using [[current]] time. 16 | Returns next fire time if successfull or null if completed." 17 | shared formal DateTime? start("current time" DateTime current); 18 | 19 | "Shifts time to the next one. 20 | Returns next fire time if successfull and null if completed." 21 | shared formal DateTime? shiftTime(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/TimeRowCronStyle.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | 3 | DateTime, 4 | dateTime 5 | } 6 | 7 | import ceylon.time.chronology { 8 | 9 | gregorian 10 | } 11 | import herd.schedule.chime.cron { 12 | 13 | CronExpression 14 | } 15 | 16 | 17 | "Ccron-like timer." 18 | since("0.1.0") by("Lis") 19 | class TimeRowCronStyle ( 20 | "Cron expression rules the timer." CronExpression expression 21 | ) 22 | satisfies TimeRow 23 | { 24 | 25 | // indexies of current time 26 | variable Iterator secondIndex = expression.seconds.iterator(); 27 | variable Iterator minuteIndex = expression.minutes.iterator(); 28 | variable Iterator hourIndex = expression.hours.iterator(); 29 | variable Iterator dayIndex = expression.daysOfMonth.iterator(); 30 | variable Iterator monthIndex = expression.months.iterator(); 31 | variable Iterator yearIndex = expression.years.iterator(); 32 | 33 | // current time 34 | variable Integer currentYear = 0; 35 | variable Integer currentMonth = 1; 36 | variable Integer currentDay = 1; 37 | variable Integer currentHour = 0; 38 | variable Integer currentMinute = 0; 39 | variable Integer currentSecond = 0; 40 | 41 | "current date and time" 42 | variable DateTime currentDate = dateTime( 0, 1, 1 ); 43 | 44 | variable Boolean completed = false; 45 | 46 | 47 | "Completes the timer." 48 | void completeTimer() { 49 | completed = true; 50 | currentDate = dateTime(0, 1, 1); 51 | secondIndex = expression.seconds.iterator(); 52 | minuteIndex = expression.minutes.iterator(); 53 | hourIndex = expression.hours.iterator(); 54 | dayIndex = expression.daysOfMonth.iterator(); 55 | monthIndex = expression.months.iterator(); 56 | yearIndex = expression.years.iterator(); 57 | } 58 | 59 | "Starts seconds from beginning." 60 | void resetSeconds() { 61 | secondIndex = expression.seconds.iterator(); 62 | if (is Integer val = secondIndex.next()) { 63 | currentSecond = val; 64 | } 65 | else { 66 | currentSecond = 0; 67 | } 68 | } 69 | 70 | "Starts minutes from beginning and reset seconds." 71 | void resetMinutes() { 72 | minuteIndex = expression.minutes.iterator(); 73 | if (is Integer val = minuteIndex.next()) { 74 | currentMinute = val; 75 | } 76 | else { 77 | currentMinute = 0; 78 | } 79 | resetSeconds(); 80 | } 81 | 82 | "Starts hours from beginning and reset minutes." 83 | void resetHours() { 84 | hourIndex = expression.hours.iterator(); 85 | if (is Integer val = hourIndex.next()) { 86 | currentHour = val; 87 | } 88 | else { 89 | currentHour = 0; 90 | } 91 | resetMinutes(); 92 | } 93 | 94 | "Start. days from beginning and reset hours." 95 | void resetDays() { 96 | dayIndex = expression.daysOfMonth.iterator(); 97 | if (is Integer val = dayIndex.next()) { 98 | currentDay = val; 99 | } 100 | else { 101 | currentDay = 1; 102 | } 103 | resetHours(); 104 | } 105 | 106 | "Starts days from beginning and reset and reset days." 107 | void resetMonth() { 108 | monthIndex = expression.months.iterator(); 109 | if (is Integer val = monthIndex.next()) { 110 | currentMonth = val; 111 | } 112 | else { 113 | currentMonth = 1; 114 | } 115 | resetDays(); 116 | } 117 | 118 | "Returns `true` if data is accepted and `false` otherwise." 119 | Boolean isDateAcepted() { 120 | value converted = gregorian.dateFrom( gregorian.fixedFrom( [currentYear, currentMonth, currentDay] ) ); 121 | return converted[0] == currentYear && converted[1] == currentMonth && converted[2] == currentDay; 122 | } 123 | 124 | "Shifts year to next after the latest fire. Next year is one from specified in [[CronExpression.years]]. 125 | If all years scooped out timing is completed. 126 | Returns `true` if completed." 127 | Boolean shiftYear() { 128 | // shift to next year 129 | if (!expression.years.empty) { 130 | if (is Integer item = yearIndex.next()) { 131 | currentYear = item; 132 | } 133 | else { 134 | return true; 135 | } 136 | } 137 | else { 138 | currentYear ++; 139 | } 140 | return false; 141 | } 142 | 143 | "Shifts month to next after the latest fire. Next month is one from specified in [[CronExpression.months]]. 144 | If all months scooped out shifts year - [[shiftYear]]. 145 | Returns `true` if completed and `false` otherwise." 146 | Boolean shiftMonth() { 147 | // shift to next month 148 | if (is Integer item = monthIndex.next()) { 149 | currentMonth = item; 150 | if (isDateAcepted()) { 151 | return false; 152 | } 153 | else { 154 | resetMonth(); 155 | return shiftYear(); 156 | } 157 | } 158 | else { 159 | resetMonth(); 160 | return shiftYear(); 161 | } 162 | } 163 | 164 | "Shifts day to next after the latest fire. Next day is one from specified in [[CronExpression.daysOfMonth]]. 165 | If all days scooped out shifts month - [[shiftMonth]]. 166 | Returns `true` if completed and `false` otherwise." 167 | Boolean shiftDay() { 168 | // shift to next day 169 | if (is Integer item = dayIndex.next()) { 170 | currentDay = item; 171 | if (isDateAcepted()) { 172 | return false; 173 | } 174 | else { 175 | resetDays(); 176 | return shiftMonth(); 177 | } 178 | } 179 | else { 180 | resetDays(); 181 | return shiftMonth(); 182 | } 183 | } 184 | 185 | "Shifts hours to next after the latest fire. Next hours are one from specified in [[CronExpression.hours]]. 186 | If all hours scooped out shifts day - [[shiftDay]]. 187 | Returns `true` if completed and `false` otherwise." 188 | Boolean shiftHour() { 189 | // shift to next hour 190 | if (is Integer item = hourIndex.next()) { 191 | currentHour = item; 192 | return false; 193 | } 194 | else { 195 | resetHours(); 196 | return shiftDay(); 197 | } 198 | } 199 | 200 | "Shifts minutes to next after the latest fire. Next minutes are one from specified in [[CronExpression.minutes]]. 201 | If all minutes scooped out shifts hours - [[shiftHour]]. 202 | Returns `true` if completed and `false` otherwise." 203 | Boolean shiftMinute() { 204 | // shift to next minute 205 | if (is Integer item = minuteIndex.next()) { 206 | currentMinute = item; 207 | return false; 208 | } 209 | else { 210 | resetMinutes(); 211 | return shiftHour(); 212 | } 213 | } 214 | 215 | "Shifts seconds to next after the latest fire. Next seconds are one from specified in [[CronExpression.seconds]]. 216 | If all seconds scooped out shifts minutes - [[shiftMinute]]. 217 | Returns `true` if completed and `false` otherwise." 218 | Boolean shiftSecond() { 219 | if (is Integer item = secondIndex.next()) { 220 | currentSecond = item; 221 | return false; 222 | } 223 | else { 224 | resetSeconds(); 225 | return shiftMinute(); 226 | } 227 | 228 | } 229 | 230 | "Considers weekdays in the [[currentDate]]. I.e. shifts days while [[currentDay]] is not within [[CronExpression.daysOfWeek]]. 231 | Returns `false` if timer to be completed and `true` otherwise." 232 | Boolean considerWeekdays() { 233 | try { 234 | variable DateTime date = dateTime(currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond); 235 | // shift to appropriate day of week 236 | variable Boolean reset = true; 237 | while (!expression.daysOfWeek.falls(date.date) && date != currentDate) { 238 | if ( reset ) { 239 | resetHours(); 240 | reset = false; 241 | } 242 | if (shiftDay()) { 243 | completeTimer(); 244 | return false; 245 | } 246 | date = dateTime(currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond); 247 | } 248 | if (date != currentDate) { 249 | currentDate = date; 250 | return true; 251 | } 252 | else { 253 | completeTimer(); 254 | return false; 255 | } 256 | } 257 | catch (Throwable err) { 258 | completeTimer(); 259 | return false; 260 | } 261 | } 262 | 263 | 264 | "Starts timing from specified UTC time. 265 | Returns `true` if started and `false` if completed." 266 | Boolean startCron(DateTime current) { 267 | // find nearest time 268 | 269 | // year 270 | if (expression.years.empty) { 271 | currentYear = current.year; 272 | } 273 | else { 274 | currentYear = 0; 275 | yearIndex = expression.years.iterator(); 276 | while (is Integer item = yearIndex.next()) { 277 | if (item >= current.year) { 278 | currentYear = item; 279 | break; 280 | } 281 | } 282 | if (currentYear == 0) { 283 | completeTimer(); 284 | return false; 285 | } 286 | else if (currentYear > current.year) { 287 | resetMonth(); 288 | return considerWeekdays(); 289 | } 290 | } 291 | 292 | // month 293 | currentMonth = 0; 294 | monthIndex = expression.months.iterator(); 295 | while (is Integer item = monthIndex.next()) { 296 | if (item >= current.month.integer) { 297 | currentMonth = item; 298 | break; 299 | } 300 | } 301 | if (currentMonth == 0) { 302 | resetMonth(); 303 | if (shiftYear()) { 304 | completeTimer(); 305 | return false; 306 | } 307 | else { 308 | return considerWeekdays(); 309 | } 310 | } 311 | if (currentMonth > current.month.integer) { 312 | resetDays(); 313 | return considerWeekdays(); 314 | } 315 | 316 | // day 317 | currentDay = 0; 318 | dayIndex = expression.daysOfMonth.iterator(); 319 | while (is Integer item = dayIndex.next()) { 320 | if (item >= current.day) { 321 | currentDay = item; 322 | break; 323 | } 324 | } 325 | if (currentDay == 0) { 326 | resetDays(); 327 | if (shiftMonth()) { 328 | completeTimer(); 329 | return false; 330 | } 331 | else { 332 | return considerWeekdays(); 333 | } 334 | } 335 | if (currentDay > current.day) { 336 | resetHours(); 337 | return considerWeekdays(); 338 | } 339 | 340 | // hour 341 | currentHour = -1; 342 | hourIndex = expression.hours.iterator(); 343 | while (is Integer item = hourIndex.next()) { 344 | if (item >= current.hours) { 345 | currentHour = item; 346 | break; 347 | } 348 | } 349 | if (currentHour == -1) { 350 | resetHours(); 351 | if (shiftDay()) { 352 | completeTimer(); 353 | return false; 354 | } 355 | else { 356 | return considerWeekdays(); 357 | } 358 | } 359 | if (currentHour > current.hours) { 360 | resetMinutes(); 361 | return considerWeekdays(); 362 | } 363 | 364 | // minutes 365 | currentMinute = -1; 366 | minuteIndex = expression.minutes.iterator(); 367 | while (is Integer item = minuteIndex.next()) { 368 | if (item >= current.minutes) { 369 | currentMinute = item; 370 | break; 371 | } 372 | } 373 | if (currentMinute == -1) { 374 | resetMinutes(); 375 | if ( shiftHour() ) { 376 | completeTimer(); 377 | return false; 378 | } 379 | else { 380 | return considerWeekdays(); 381 | } 382 | } 383 | if (currentMinute > current.minutes) { 384 | resetSeconds(); 385 | return considerWeekdays(); 386 | } 387 | 388 | // seconds 389 | currentSecond = -1; 390 | secondIndex = expression.seconds.iterator(); 391 | while (is Integer item = secondIndex.next()) { 392 | if (item >= current.seconds) { 393 | currentSecond = item; 394 | break; 395 | } 396 | } 397 | if (currentSecond == -1) { 398 | resetSeconds(); 399 | if (shiftMinute()) { 400 | completeTimer(); 401 | return false; 402 | } 403 | } 404 | 405 | // shift to appropriate day of week 406 | return considerWeekdays(); 407 | 408 | } 409 | 410 | "Calculates next local time and stores it in [[currentDate]]. 411 | Returns `true` if successfully shifted and `false` if to be completed." 412 | Boolean shiftCronTime() { 413 | if (shiftSecond()) { 414 | completeTimer(); 415 | return false; 416 | } 417 | else { 418 | return considerWeekdays(); 419 | } 420 | } 421 | 422 | 423 | /* Timer interface */ 424 | 425 | shared actual DateTime? start(DateTime current) { 426 | if (startCron(current)) { 427 | return currentDate; 428 | } 429 | else { 430 | return null; 431 | } 432 | } 433 | 434 | shared actual DateTime? shiftTime() { 435 | if (shiftCronTime()) { 436 | return currentDate; 437 | } 438 | else { 439 | return null; 440 | } 441 | } 442 | 443 | shared actual String string => "cron time row"; 444 | 445 | } 446 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/TimeRowFactory.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.json { 2 | 3 | JsonObject 4 | } 5 | import herd.schedule.chime.service { 6 | ChimeServices, 7 | Extension 8 | } 9 | 10 | 11 | "Creates [[TimeRow]]." 12 | since("0.1.0") by("Lis") 13 | shared interface TimeRowFactory satisfies Extension 14 | { 15 | 16 | "Creates new time row. Returns created [[TimeRow]] or error code -> message pair if some error occured." 17 | shared actual formal TimeRow|String> create ( 18 | "Provides Chime services." ChimeServices services, 19 | "Timer description." JsonObject description 20 | ); 21 | 22 | "Timer type provided with timer create request, see [[module herd.schedule.chime]]." 23 | shared actual formal String type; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/TimeRowInterval.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | 3 | DateTime, 4 | dateTime 5 | } 6 | 7 | 8 | "Incremental timer - starts from specific time date 9 | and increments on certain miliseconds each time when fired - [[intervalMilliseconds]]. 10 | [[intervalMilliseconds]] to be >= 0. 11 | " 12 | since("0.1.0") by("Lis") 13 | class TimeRowInterval ( 14 | "Timing delay in miliseconds, to be >= 0." shared Integer intervalMilliseconds 15 | ) 16 | satisfies TimeRow 17 | { 18 | "Current date and time." 19 | variable DateTime currentDate = dateTime(0, 1, 1); 20 | 21 | 22 | shared actual DateTime? start(DateTime current) => currentDate = current.plusMilliseconds(intervalMilliseconds); 23 | 24 | shared actual DateTime? shiftTime() => currentDate = currentDate.plusMilliseconds(intervalMilliseconds); 25 | 26 | shared actual String string => "interval time row with delay of ``intervalMilliseconds`` ms"; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/TimeRowUnion.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | import ceylon.collection { 5 | ArrayList 6 | } 7 | 8 | 9 | "Represents a union of the time rows." 10 | since("0.2.1") by("Lis") 11 | shared class TimeRowUnion satisfies TimeRow { 12 | 13 | static class InternalRow(TimeRow row) satisfies TimeRow { 14 | variable DateTime? currentDate_ = null; 15 | shared DateTime? currentDate => currentDate_; 16 | shared actual DateTime? shiftTime() { 17 | return (currentDate_ = row.shiftTime()); 18 | } 19 | shared actual DateTime? start(DateTime current) { 20 | return (currentDate_ = row.start(current)); 21 | } 22 | shared actual String string => row.string; 23 | } 24 | 25 | static object emptyInternalRow extends InternalRow(emptyTimeRow) {} 26 | 27 | 28 | "Currently selected row, i.e. with min date. Selected at [[shiftTime]] or [[start]]." 29 | variable InternalRow currentRow = emptyInternalRow; 30 | "List of currently active time rows." 31 | ArrayList timeRows; 32 | 33 | 34 | "Instantiates new time row union from a list of time rows." 35 | shared new ("Time rows for union." {TimeRow+} rows) { 36 | timeRows = ArrayList{for (item in rows) InternalRow(item)}; 37 | } 38 | 39 | 40 | "Removes completed time rows from [[timeRows]] list." 41 | void removeCompleted() { 42 | value toRemove = [for (item in timeRows) if (!item.currentDate exists) item]; 43 | timeRows.removeAll(toRemove); 44 | if (timeRows.empty) { 45 | currentRow = emptyInternalRow; 46 | } 47 | } 48 | 49 | 50 | shared actual DateTime? shiftTime() { 51 | currentRow.shiftTime(); 52 | currentRow = emptyInternalRow; 53 | for (item in timeRows) { 54 | if (exists n = currentRow.currentDate) { 55 | if (exists itemDate = item.currentDate, itemDate < n) { 56 | currentRow = item; 57 | } 58 | } 59 | else { 60 | currentRow = item; 61 | } 62 | } 63 | value ret = currentRow.currentDate; 64 | removeCompleted(); 65 | return ret; 66 | } 67 | 68 | shared actual DateTime? start(DateTime current) { 69 | currentRow = emptyInternalRow; 70 | for (item in timeRows) { 71 | if (exists itemDate = item.start(current)) { 72 | if (exists l = currentRow.currentDate) { 73 | if (itemDate < l) { 74 | currentRow = item; 75 | } 76 | } 77 | else { 78 | currentRow = item; 79 | } 80 | } 81 | } 82 | value ret = currentRow.currentDate; 83 | removeCompleted(); 84 | return ret; 85 | } 86 | 87 | shared actual String string => "union time row: ``timeRows``"; 88 | 89 | } 90 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/UnionFactory.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.collection { 2 | ArrayList 3 | } 4 | import ceylon.json { 5 | JsonObject, 6 | JsonArray 7 | } 8 | import herd.schedule.chime { 9 | Chime 10 | } 11 | import herd.schedule.chime.service { 12 | ChimeServices, 13 | Extension 14 | } 15 | 16 | 17 | "Factory to create union timers." 18 | service(`interface Extension`) 19 | since("0.3.0") by("Lis") 20 | shared class UnionFactory() satisfies TimeRowFactory 21 | { 22 | 23 | shared actual String type => Chime.type.union; 24 | 25 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) { 26 | if (is JsonArray timers = description[Chime.key.timers]) { 27 | ArrayList timeRows = ArrayList(); 28 | for (timer in timers) { 29 | if (is JsonObject timer) { 30 | value ret = services.createTimeRow(timer); 31 | if (is TimeRow ret) { 32 | timeRows.add(ret); 33 | } 34 | else { 35 | return ret; 36 | } 37 | } 38 | else { 39 | return Chime.errors.codeNotJSONTimerDescription->Chime.errors.notJSONTimerDescription; 40 | } 41 | } 42 | if (nonempty unionRows = timeRows.sequence()) { 43 | return TimeRowUnion(unionRows); 44 | } 45 | else { 46 | return Chime.errors.codeTimersListHasToBeSpecified->Chime.errors.timersListHasToBeSpecified; 47 | } 48 | } 49 | else { 50 | return Chime.errors.codeTimersListHasToBeSpecified->Chime.errors.timersListHasToBeSpecified; 51 | } 52 | } 53 | 54 | shared actual String string => "union time row factory"; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/emptyTimeRow.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | 5 | 6 | "`TimeRow` which return `null`." 7 | since("0.3.0") by("Lis") 8 | object emptyTimeRow satisfies TimeRow { 9 | shared actual DateTime? shiftTime() => null; 10 | shared actual DateTime? start(DateTime current) => null; 11 | } 12 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timer/package.ceylon: -------------------------------------------------------------------------------- 1 | "Provides timer services. 2 | 3 | Built-in and custom timers are provided with [[TimeRow]] interface. 4 | The interface is similar to time enumerator, but allows to restart enumeration from some time. 5 | [[TimeRow]] is created using service provider represented with [[TimeRowFactory]] interface. 6 | 7 | Built-in time rows and factories: 8 | * cron-style time row created by [[CronFactory]] 9 | * interval time row created by [[IntervalFactory]] 10 | * union time row created by [[UnionFactory]] 11 | 12 | To build your own [[TimeRow]] (i.e. timer) follow the steps: 13 | 1. Implement [[TimeRowFactory]]. 14 | 2. Mark the class from 1. with `service(`\` `interface Extension`\` `)`. 15 | 3. Deploy _Chime_ with configuration provided modules to serach the services: 16 | JsonObject { 17 | \"services\" -> [\"module name/module version\"] 18 | } 19 | 20 | " 21 | since("0.3.0") by("Lis") 22 | shared package herd.schedule.chime.service.timer; 23 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timezone/JVMTimeZone.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | Instant, 3 | DateTime 4 | } 5 | import java.util { 6 | JavaTimeZone=TimeZone 7 | } 8 | 9 | 10 | "Converts date/time using time zones available at JVM according to specified remote time zone." 11 | since("0.1.0") by("Lis") 12 | class JVMTimeZone satisfies TimeZone { 13 | 14 | static JavaTimeZone localtz = JavaTimeZone.default; 15 | 16 | JavaTimeZone remoteTimeZone; 17 | 18 | shared new ("Time zone to link this one to." JavaTimeZone remoteTimeZone) { 19 | this.remoteTimeZone = remoteTimeZone; 20 | } 21 | 22 | shared actual DateTime toLocal(DateTime remote) { 23 | Integer remoteTime = remote.instant().millisecondsOfEpoch; 24 | Integer utcTime = remoteTime - remoteTimeZone.getOffset(remoteTime); 25 | Integer localUTCOffset = localtz.getOffset(utcTime + localtz.getOffset(utcTime)); 26 | return Instant(utcTime + localUTCOffset).dateTime(); 27 | } 28 | 29 | shared actual DateTime toRemote(DateTime local) { 30 | Integer localTime = local.instant().millisecondsOfEpoch; 31 | Integer utcTime = localTime - localtz.getOffset(localTime); 32 | Integer remoteUTCOffset = remoteTimeZone.getOffset(utcTime + remoteTimeZone.getOffset(utcTime)); 33 | return Instant(utcTime + remoteUTCOffset).dateTime(); 34 | } 35 | 36 | shared actual String timeZoneID => remoteTimeZone.id; 37 | 38 | shared actual String string => timeZoneID; 39 | } 40 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timezone/JVMTimeZoneFactory.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime { 2 | Chime 3 | } 4 | import java.util { 5 | JavaTimeZone=TimeZone 6 | } 7 | import herd.schedule.chime.service { 8 | ChimeServices, 9 | Extension 10 | } 11 | import ceylon.time { 12 | DateTime 13 | } 14 | import ceylon.json { 15 | JsonObject 16 | } 17 | 18 | 19 | "Creates [[TimeZone]] using JVM time zones." 20 | service(`interface Extension`) 21 | since("0.3.0") by( "Lis") 22 | shared class JVMTimeZoneFactory satisfies TimeZoneFactory 23 | { 24 | 25 | "Defines time zone which does no convertion." 26 | shared static object localTimeZone satisfies TimeZone { 27 | shared actual DateTime toLocal(DateTime remote) => remote; 28 | shared actual DateTime toRemote(DateTime local) => local; 29 | shared actual String timeZoneID => JavaTimeZone.default.id; 30 | } 31 | 32 | "New `JVMTimeZoneFactory`." 33 | shared new () {} 34 | 35 | shared actual TimeZone|String> create(ChimeServices services, JsonObject options) { 36 | if (is String timeZone = options[Chime.key.timeZone]) { 37 | JavaTimeZone tz = JavaTimeZone.getTimeZone(timeZone); 38 | if (tz.id == timeZone) { 39 | return JVMTimeZone(tz); 40 | } 41 | else { 42 | return Chime.errors.codeUnsupportedTimezone->Chime.errors.unsupportedTimezone; 43 | } 44 | } 45 | else { 46 | return localTimeZone; 47 | } 48 | 49 | } 50 | 51 | shared actual String type => Chime.timeZoneProvider.jvm; 52 | 53 | shared actual String string => "JVM time zone factory"; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timezone/TimeZone.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.time { 2 | DateTime 3 | } 4 | 5 | 6 | "Convertes date-time according to rule (timezone). 7 | Generally, a time zone is instantiated by [[TimeZoneFactory]] given as service provider, 8 | see [[package herd.schedule.chime.service]]." 9 | see(`interface TimeZoneFactory`) 10 | since("0.3.0") by("Lis") 11 | shared interface TimeZone { 12 | 13 | "Converts remote (this time zone) date-time to local (for the current machine) one." 14 | shared formal DateTime toLocal("Date-time to convert from." DateTime remote); 15 | 16 | "Converts local (for the current machine) date-time to remote (to this time zone) one." 17 | shared formal DateTime toRemote("Date-time to convert from." DateTime local); 18 | 19 | "Returns time zone id." 20 | shared formal String timeZoneID; 21 | } 22 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timezone/TimeZoneFactory.ceylon: -------------------------------------------------------------------------------- 1 | import herd.schedule.chime.service { 2 | Extension, 3 | ChimeServices 4 | } 5 | import ceylon.json { 6 | JsonObject 7 | } 8 | 9 | 10 | "Time zone provider - creates [[TimeZone]]." 11 | since("0.3.0") by("Lis") 12 | shared interface TimeZoneFactory satisfies Extension 13 | { 14 | "Creates new time zone with the given time zone name. 15 | Returns created [[TimeZone]] or error code -> message pair if some error occured." 16 | shared actual formal TimeZone|String> create ( 17 | "Provides Chime services." ChimeServices services, 18 | "Options with \"time zone\" key given." JsonObject options 19 | ); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /source/herd/schedule/chime/service/timezone/package.ceylon: -------------------------------------------------------------------------------- 1 | 2 | "Provides time zone services. 3 | 4 | [[TimeZone]] interface provides converting time from / to the given time zone. 5 | 6 | built-in time zones: 7 | * time zones available at jvm, created by [[JVMTimeZoneFactory]]. 8 | 9 | To build your own [[TimeZone]] follow the steps: 10 | 1. Implement [[TimeZoneFactory]]. 11 | 2. Mark the class from 1. with `service(`\` `interface TimeZoneFactory`\` `)`. 12 | 3. Deploy _Chime_ with configuration provided modules to serach the services: 13 | JsonObject { 14 | \"services\" -> [\"module name/module version\"] 15 | } 16 | 17 | In timer or scheduler create request time zone type can be specified under \"time zone provider\" key. 18 | 19 | > At scheduler level default time zone may be specified, which is applied to timer if no time zone 20 | is given at timer create request. 21 | " 22 | since("0.3.0") by("Lis") 23 | shared package herd.schedule.chime.service.timezone; 24 | -------------------------------------------------------------------------------- /test/herd/test/schedule/chime/CronBuilder.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.test { 2 | test 3 | } 4 | import herd.asynctest { 5 | AsyncTestContext 6 | } 7 | import herd.schedule.chime { 8 | CronBuilder, 9 | Chime 10 | } 11 | import ceylon.json { 12 | 13 | JsonObject 14 | } 15 | import herd.asynctest.match { 16 | ItemByKey, 17 | ExceptionHasType 18 | } 19 | 20 | class CronBuilderTest() { 21 | 22 | shared test void cronBuilderWith( AsyncTestContext context ) { 23 | JsonObject cron = CronBuilder().withSeconds(1,2).withMinutes(1,2).withHours(1,2) 24 | .withDays(1,2).withMonths(1,2).withYears(2017).withDaysOfWeek(1,2).build(); 25 | context.assertThat ( 26 | cron, 27 | ItemByKey( "type", Chime.type.cron ) 28 | ); 29 | context.assertThat ( 30 | cron, 31 | ItemByKey( "seconds", "1,2" ) 32 | ); 33 | context.assertThat ( 34 | cron, 35 | ItemByKey( "minutes", "1,2" ) 36 | ); 37 | context.assertThat ( 38 | cron, 39 | ItemByKey( "hours", "1,2" ) 40 | ); 41 | context.assertThat ( 42 | cron, 43 | ItemByKey( "days of month", "1,2" ) 44 | ); 45 | context.assertThat ( 46 | cron, 47 | ItemByKey( "months", "1,2" ) 48 | ); 49 | context.assertThat ( 50 | cron, 51 | ItemByKey( "years", "2017" ) 52 | ); 53 | context.assertThat ( 54 | cron, 55 | ItemByKey( "days of week", "1,2" ) 56 | ); 57 | context.complete(); 58 | } 59 | 60 | shared test void cronBuilderWithAll( AsyncTestContext context ) { 61 | JsonObject cron = CronBuilder().withAllSeconds().withAllMinutes().withAllHours() 62 | .withAllDays().withAllMonths().withAllYears().withAllDaysOfWeek().build(); 63 | context.assertThat ( 64 | cron, 65 | ItemByKey( "type", Chime.type.cron ) 66 | ); 67 | context.assertThat ( 68 | cron, 69 | ItemByKey( "seconds", "*" ) 70 | ); 71 | context.assertThat ( 72 | cron, 73 | ItemByKey( "minutes", "*" ) 74 | ); 75 | context.assertThat ( 76 | cron, 77 | ItemByKey( "hours", "*" ) 78 | ); 79 | context.assertThat ( 80 | cron, 81 | ItemByKey( "days of month", "*" ) 82 | ); 83 | context.assertThat ( 84 | cron, 85 | ItemByKey( "months", "*" ) 86 | ); 87 | context.assertThat ( 88 | cron, 89 | ItemByKey( "years", "*" ) 90 | ); 91 | context.assertThat ( 92 | cron, 93 | ItemByKey( "days of week", "*" ) 94 | ); 95 | context.complete(); 96 | } 97 | 98 | shared test void cronBuilderWithRange( AsyncTestContext context ) { 99 | JsonObject cron = CronBuilder().withSecondsRange(1,5,2).withMinutesRange(1,5,2).withHoursRange(1,5,2) 100 | .withDaysRange(1,5,2).withMonthsRange(1,5,2).withYearsRange(2017,2021,2).withDaysOfWeekRange(1,5,2).build(); 101 | context.assertThat ( 102 | cron, 103 | ItemByKey( "type", Chime.type.cron ) 104 | ); 105 | context.assertThat ( 106 | cron, 107 | ItemByKey( "seconds", "1-5/2" ) 108 | ); 109 | context.assertThat ( 110 | cron, 111 | ItemByKey( "minutes", "1-5/2" ) 112 | ); 113 | context.assertThat ( 114 | cron, 115 | ItemByKey( "hours", "1-5/2" ) 116 | ); 117 | context.assertThat ( 118 | cron, 119 | ItemByKey( "days of month", "1-5/2" ) 120 | ); 121 | context.assertThat ( 122 | cron, 123 | ItemByKey( "months", "1-5/2" ) 124 | ); 125 | context.assertThat ( 126 | cron, 127 | ItemByKey( "years", "2017-2021/2" ) 128 | ); 129 | context.assertThat ( 130 | cron, 131 | ItemByKey( "days of week", "1-5/2" ) 132 | ); 133 | context.complete(); 134 | } 135 | 136 | shared test void cronBuilderFromBuilder( AsyncTestContext context ) { 137 | CronBuilder cronBuilder = CronBuilder().withSeconds(1,2).withMinutes(1,2).withHours(1,2); 138 | JsonObject cron = CronBuilder.fromBuilder(cronBuilder).withDays(1,2).withMonths(1,2).build(); 139 | context.assertThat ( 140 | cron, 141 | ItemByKey( "type", Chime.type.cron ) 142 | ); 143 | context.assertThat ( 144 | cron, 145 | ItemByKey( "seconds", "1,2" ) 146 | ); 147 | context.assertThat ( 148 | cron, 149 | ItemByKey( "minutes", "1,2" ) 150 | ); 151 | context.assertThat ( 152 | cron, 153 | ItemByKey( "hours", "1,2" ) 154 | ); 155 | context.assertThat ( 156 | cron, 157 | ItemByKey( "days of month", "1,2" ) 158 | ); 159 | context.assertThat ( 160 | cron, 161 | ItemByKey( "months", "1,2" ) 162 | ); 163 | context.complete(); 164 | } 165 | shared test void cronBuilderIncomplete( AsyncTestContext context ) { 166 | CronBuilder cron = CronBuilder().withSecondsRange(1,5,2); 167 | context.assertThatException ( 168 | cron.build, 169 | ExceptionHasType() 170 | ); 171 | context.complete(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test/herd/test/schedule/chime/SchedulerTimer.ceylon: -------------------------------------------------------------------------------- 1 | import ceylon.test { 2 | beforeTestRun, 3 | afterTestRun, 4 | test 5 | } 6 | import herd.asynctest { 7 | AsyncPrePostContext, 8 | AsyncTestContext 9 | } 10 | import io.vertx.ceylon.core { 11 | Vertx, 12 | vertx 13 | } 14 | import herd.schedule.chime { 15 | Chime, 16 | Scheduler, 17 | createScheduler, 18 | schedulerInfo, 19 | delete, 20 | Timer, 21 | TimerEvent, 22 | TimerInfo, 23 | SchedulerInfo, 24 | TimerFire, 25 | TimerCompleted 26 | } 27 | import io.vertx.ceylon.core.eventbus { 28 | EventBus, 29 | Message 30 | } 31 | import herd.asynctest.match { 32 | EqualTo, 33 | ExceptionHasMessage, 34 | PassType, 35 | SizeOf 36 | } 37 | import ceylon.json { 38 | JsonObject 39 | } 40 | 41 | 42 | "Testing `Scheduler` and `Timer` interfaces and scheduler top level functions." 43 | since( "0.2.0" ) by( "Lis" ) 44 | shared class SchedulerTimer() 45 | { 46 | String chime = "chime"; 47 | Vertx v = vertx.vertx(); 48 | EventBus eventBus = v.eventBus(); 49 | 50 | 51 | shared afterTestRun void dispose() { 52 | v.close(); 53 | } 54 | 55 | shared beforeTestRun void initialize(AsyncPrePostContext initContext) { 56 | Chime c = Chime(); 57 | c.deploy ( 58 | v, null, 59 | (Throwable|String res) { 60 | if (is String res) { 61 | initContext.proceed(); 62 | } 63 | else { 64 | initContext.abort(res, "Chime starting"); 65 | } 66 | } 67 | ); 68 | 69 | } 70 | 71 | test shared void createDeleteScheduler(AsyncTestContext context) { 72 | String schedulerName = "createDeleteScheduler"; 73 | createScheduler ( 74 | (Throwable|Scheduler msg) { 75 | if (is Scheduler msg) { 76 | context.assertThat(msg.name, EqualTo( schedulerName)); 77 | msg.delete(); 78 | eventBus.send ( 79 | chime, 80 | JsonObject { 81 | Chime.key.operation -> Chime.operation.info, 82 | Chime.key.name -> schedulerName 83 | }, 84 | (Throwable|Message msg) { 85 | context.assertThat(msg, PassType(ExceptionHasMessage(Chime.errors.schedulerNotExists))); 86 | context.complete(); 87 | } 88 | ); 89 | } 90 | else { 91 | context.fail(msg); 92 | context.complete(); 93 | } 94 | }, 95 | chime, eventBus, schedulerName 96 | ); 97 | } 98 | 99 | 100 | test shared void createDeleteTimer(AsyncTestContext context) { 101 | String schedulerName = "createDeleteTimer"; 102 | createScheduler ( 103 | (Throwable|Scheduler msg) { 104 | if (is Scheduler msg) { 105 | msg.createIntervalTimer ( 106 | (Throwable|Timer timer) { 107 | if (is Timer timer) { 108 | timer.handler ( 109 | (TimerEvent event) { 110 | timer.delete(); 111 | timer.info ( 112 | (Throwable|TimerInfo info) { 113 | context.assertThat ( 114 | info, PassType(ExceptionHasMessage(Chime.errors.timerNotExists)) 115 | ); 116 | msg.delete(); 117 | context.complete(); 118 | } 119 | ); 120 | } 121 | ); 122 | } 123 | else { 124 | msg.delete(); 125 | context.fail(timer); 126 | context.complete(); 127 | } 128 | }, 129 | 1 130 | ); 131 | } 132 | else { 133 | context.fail(msg); 134 | context.complete(); 135 | } 136 | }, 137 | chime, eventBus, schedulerName 138 | ); 139 | } 140 | 141 | 142 | void createTimer(String schedulerName, String timerName, Integer delay) { 143 | eventBus.send ( 144 | chime, 145 | JsonObject { 146 | Chime.key.operation -> Chime.operation.create, 147 | Chime.key.name -> schedulerName + Chime.configuration.nameSeparator + timerName, 148 | Chime.key.description -> JsonObject { 149 | Chime.key.type -> Chime.type.interval, 150 | Chime.key.delay -> delay 151 | } 152 | } 153 | ); 154 | } 155 | 156 | 157 | test shared void deleteTimers(AsyncTestContext context) { 158 | String scheduler1 = "scheduler1"; 159 | String scheduler2 = "scheduler2"; 160 | String timer1 = "timer1"; 161 | String timer2 = "timer2"; 162 | String timer1Full = scheduler1 + ":" + timer1; 163 | String timer2Full = scheduler2 + ":" + timer2; 164 | 165 | // deletes all schedulers if created before this test 166 | delete( chime, eventBus ); 167 | 168 | createTimer(scheduler1, timer1, 5); 169 | createTimer(scheduler1, timer2, 7); 170 | createTimer(scheduler2, timer1, 4); 171 | createTimer(scheduler2, timer2, 9); 172 | 173 | delete ( 174 | chime, eventBus, {timer1Full, timer2Full}, 175 | (Throwable|{String*} msg) { 176 | if (is Throwable msg) { 177 | delete(chime, eventBus); 178 | context.fail(msg); 179 | context.complete(); 180 | } 181 | else { 182 | context.assertThat(msg, SizeOf(2)); 183 | if (exists timer1Name = msg.first, exists timer2Name = msg.last) { 184 | context.assertThat(timer1Name, EqualTo( timer1Full)); 185 | context.assertThat(timer2Name, EqualTo( timer2Full)); 186 | } 187 | else { 188 | context.fail(AssertionError("Returned list of deleted items is empty.")); 189 | } 190 | delete(chime, eventBus); 191 | context.complete(); 192 | } 193 | } 194 | ); 195 | } 196 | 197 | 198 | test shared void deleteSchedulers(AsyncTestContext context) { 199 | String scheduler1 = "scheduler1"; 200 | String scheduler2 = "scheduler2"; 201 | String timer1 = "timer1"; 202 | String timer2 = "timer2"; 203 | 204 | // deletes all schedulers if created before this test 205 | delete( chime, eventBus ); 206 | 207 | createTimer(scheduler1, timer1, 5); 208 | createTimer(scheduler1, timer2, 7); 209 | createTimer(scheduler2, timer1, 4); 210 | createTimer(scheduler2, timer2, 9); 211 | 212 | delete ( 213 | chime, eventBus, {scheduler1, scheduler2}, 214 | (Throwable|{String*} msg) { 215 | if (is Throwable msg) { 216 | delete(chime, eventBus); 217 | context.fail(msg); 218 | context.complete(); 219 | } 220 | else { 221 | context.assertThat(msg, SizeOf(2)); 222 | if (exists scheduler1Name = msg.first, exists scheduler2Name = msg.last) { 223 | context.assertThat(scheduler1Name, EqualTo( scheduler1)); 224 | context.assertThat(scheduler2Name, EqualTo( scheduler2)); 225 | } 226 | else { 227 | context.fail(AssertionError("Returned list of deleted items is empty.")); 228 | } 229 | delete(chime, eventBus); 230 | context.complete(); 231 | } 232 | } 233 | ); 234 | } 235 | 236 | 237 | test shared void getSchedulerInfo(AsyncTestContext context) { 238 | String scheduler1 = "info1"; 239 | String scheduler2 = "info2"; 240 | String timer1 = "timer1"; 241 | String timer2 = "timer2"; 242 | 243 | // deletes all schedulers if created before this test 244 | delete(chime, eventBus); 245 | 246 | createTimer(scheduler1, timer1, 5); 247 | createTimer(scheduler1, timer2, 7); 248 | createTimer(scheduler2, timer1, 4); 249 | createTimer(scheduler2, timer2, 9); 250 | 251 | schedulerInfo ( 252 | (Throwable|SchedulerInfo[] msg) { 253 | if (is Throwable msg) { 254 | delete(chime, eventBus); 255 | context.fail(msg); 256 | context.complete(); 257 | } 258 | else { 259 | context.assertThat(msg, SizeOf(2)); 260 | if (exists sch1 = msg.first, exists sch2 = msg.last) { 261 | context.assertThat(sch1.timers, SizeOf(2)); 262 | context.assertThat(sch2.timers, SizeOf(2)); 263 | context.assertThat(sch1.name, EqualTo( scheduler1)); 264 | context.assertThat(sch2.name, EqualTo( scheduler2)); 265 | } 266 | else { 267 | context.fail(AssertionError("Returned list of infos is empty.")); 268 | } 269 | delete(chime, eventBus); 270 | context.complete(); 271 | } 272 | }, 273 | chime, eventBus 274 | ); 275 | } 276 | 277 | test shared void timerMessage(AsyncTestContext context) { 278 | String schedulerName = "TimerMessage"; 279 | String timerMessage = "message"; 280 | createScheduler ( 281 | (Throwable|Scheduler msg) { 282 | if (is Scheduler msg) { 283 | msg.createIntervalTimer { 284 | handler = (Throwable|Timer timer) { 285 | if (is Timer timer) { 286 | timer.handler ( 287 | (TimerEvent event) { 288 | switch (event) 289 | case (is TimerFire) { 290 | context.assertThat ( 291 | event.message, PassType(EqualTo(timerMessage)) 292 | ); 293 | } 294 | case (is TimerCompleted) { 295 | msg.delete(); 296 | context.complete(); 297 | } 298 | 299 | timer.delete(); 300 | timer.info ( 301 | (Throwable|TimerInfo info) { 302 | context.assertThat ( 303 | info, PassType(ExceptionHasMessage(Chime.errors.timerNotExists)) 304 | ); 305 | msg.delete(); 306 | context.complete(); 307 | } 308 | ); 309 | } 310 | ); 311 | } 312 | else { 313 | msg.delete(); 314 | context.fail(timer); 315 | context.complete(); 316 | } 317 | }; 318 | delay = 1; 319 | maxCount = 1; 320 | message = timerMessage; 321 | }; 322 | } 323 | else { 324 | context.fail(msg); 325 | context.complete(); 326 | } 327 | }, 328 | chime, eventBus, schedulerName 329 | ); 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /test/herd/test/schedule/chime/SimpleTimers.ceylon: -------------------------------------------------------------------------------- 1 | import herd.asynctest { 2 | 3 | AsyncTestContext, 4 | AsyncPrePostContext 5 | } 6 | import io.vertx.ceylon.core { 7 | 8 | Vertx, 9 | vertx 10 | } 11 | import io.vertx.ceylon.core.eventbus { 12 | 13 | EventBus, 14 | Message 15 | } 16 | import ceylon.test { 17 | 18 | test, 19 | afterTestRun, 20 | beforeTestRun 21 | } 22 | import ceylon.json { 23 | 24 | JsonObject, 25 | JsonArray 26 | } 27 | import herd.asynctest.match { 28 | 29 | EqualTo, 30 | PassType, 31 | NotEmpty 32 | } 33 | import herd.schedule.chime { 34 | 35 | Chime 36 | } 37 | 38 | 39 | since( "0.2.0" ) by( "Lis" ) 40 | shared class SimpleTimers() 41 | { 42 | 43 | String chime = "chime"; 44 | 45 | String scheduler = "scheduler"; 46 | 47 | String interval = "scheduler:interval"; 48 | String cron = "scheduler:cron"; 49 | String union = "scheduler:union"; 50 | 51 | 52 | Vertx v = vertx.vertx(); 53 | EventBus eventBus = v.eventBus(); 54 | 55 | 56 | shared afterTestRun void dispose() { 57 | v.close(); 58 | } 59 | 60 | shared beforeTestRun void initialize(AsyncPrePostContext initContext) { 61 | Chime c = Chime(); 62 | c.deploy ( 63 | v, null, 64 | (String|Throwable res) { 65 | if (is String res) { 66 | setupScheduler(initContext); 67 | } 68 | else { 69 | initContext.abort(res, "Chime starting"); 70 | } 71 | } 72 | ); 73 | 74 | } 75 | 76 | void setupScheduler(AsyncPrePostContext initContext) { 77 | eventBus.send ( 78 | chime, 79 | JsonObject { 80 | Chime.key.operation -> Chime.operation.create, 81 | Chime.key.name -> scheduler, 82 | Chime.key.state -> Chime.state.running 83 | }, 84 | (Throwable | Message msg) { 85 | if (is Message msg) { 86 | initContext.proceed(); 87 | } 88 | else { 89 | initContext.abort(msg, "Scheduler creation"); 90 | } 91 | } 92 | ); 93 | 94 | } 95 | 96 | 97 | Anything(Throwable | Message) timerValidation ( 98 | String timerName, Integer delay, Integer max, AsyncTestContext context 99 | ) { 100 | variable Integer fireCount = 0; 101 | variable Integer? previousTime = null; 102 | variable Integer totalDelay = 0; 103 | return (Throwable | Message msg) { 104 | if (is Message msg) { 105 | if (exists body = msg.body()) { 106 | if (is String event = body[Chime.key.event]) { 107 | if (event == Chime.event.complete) { 108 | context.assertThat(fireCount, EqualTo(max), "Total number of fires for ``timerName``"); 109 | context.assertThat(totalDelay, EqualTo(delay * (max - 1)), "Total delay seconds for ``timerName``"); 110 | context.complete(); 111 | } 112 | else if (event == Chime.event.fire) { 113 | fireCount ++; 114 | if (is Integer seconds = body[Chime.date.seconds]) { 115 | if (exists prev = previousTime) { 116 | if (seconds > prev) { 117 | totalDelay += seconds - prev; 118 | } 119 | else { 120 | totalDelay += seconds + 60 - prev; 121 | } 122 | } 123 | previousTime = seconds; 124 | } 125 | else { 126 | context.fail ( 127 | Exception("Chime timer fires without timer seconds ('seconds' field)"), 128 | "Chime timer ``timerName`` fire" 129 | ); 130 | context.complete(); 131 | } 132 | } 133 | else { 134 | context.fail ( 135 | Exception("Chime timer event has to be one of FIRE or COMPLETE"), 136 | "Chime timer ``timerName`` fire" 137 | ); 138 | context.complete(); 139 | } 140 | } 141 | else { 142 | context.fail ( 143 | Exception("Chime timer event without timer event specification ('event' field)"), 144 | "Chime timer ``timerName`` event" 145 | ); 146 | context.complete(); 147 | } 148 | } 149 | else { 150 | context.fail ( 151 | Exception("Chime timer fires with null message"), 152 | "Chime timer ``timerName`` fire" 153 | ); 154 | context.complete(); 155 | } 156 | } 157 | else { 158 | context.fail(msg); 159 | context.complete(); 160 | } 161 | }; 162 | } 163 | 164 | 165 | test shared void generateTimerID(AsyncTestContext context) { 166 | eventBus.send ( 167 | chime, 168 | JsonObject { 169 | Chime.key.operation -> Chime.operation.create, 170 | Chime.key.maxCount -> 1, 171 | Chime.key.description -> JsonObject { 172 | Chime.key.type -> Chime.type.interval, 173 | Chime.key.delay -> 1 174 | } 175 | }, 176 | (Throwable|Message msg) { 177 | if (is Throwable msg) { 178 | context.fail(msg, "Automatic timer ID generation"); 179 | context.complete(); 180 | } 181 | else { 182 | if (exists resp = msg.body()) { 183 | context.assertThat ( 184 | resp[Chime.key.name], PassType(NotEmpty()), "", true 185 | ); 186 | context.complete(); 187 | } 188 | else { 189 | context.fail(Exception("Chime rejects to setup timer")); 190 | context.complete(); 191 | } 192 | } 193 | } 194 | ); 195 | } 196 | 197 | test shared void intervalTimer(AsyncTestContext context) { 198 | 199 | Integer timerDelay = 1; 200 | 201 | eventBus.consumer ( 202 | interval, 203 | timerValidation(interval, timerDelay, 3, context) 204 | ); 205 | 206 | eventBus.send ( 207 | chime, 208 | JsonObject { 209 | Chime.key.operation -> Chime.operation.create, 210 | Chime.key.name -> interval, 211 | Chime.key.maxCount -> 3, 212 | Chime.key.description -> JsonObject { 213 | Chime.key.type -> Chime.type.interval, 214 | Chime.key.delay -> timerDelay 215 | } 216 | }, 217 | (Throwable|Message msg) { 218 | if (is Throwable msg) { 219 | context.fail(msg, "Interval timer setup"); 220 | context.complete(); 221 | } 222 | else { 223 | if (!msg.body() exists) { 224 | context.fail(Exception("Chime rejects to setup interval timer"), "Interval timer setup"); 225 | context.complete(); 226 | } 227 | } 228 | } 229 | ); 230 | 231 | } 232 | 233 | test shared void cronTimer(AsyncTestContext context) { 234 | 235 | eventBus.consumer ( 236 | cron, 237 | timerValidation(cron, 1, 3, context) 238 | ); 239 | 240 | eventBus.send ( 241 | chime, 242 | JsonObject { 243 | Chime.key.operation -> Chime.operation.create, 244 | Chime.key.name -> cron, 245 | Chime.key.maxCount -> 3, 246 | Chime.key.description -> JsonObject { 247 | Chime.key.type -> Chime.type.cron, 248 | Chime.date.seconds -> "0-59", 249 | Chime.date.minutes -> "*", 250 | Chime.date.hours -> "0-23", 251 | Chime.date.daysOfMonth -> "1-31", 252 | Chime.date.months -> "*", 253 | Chime.date.daysOfWeek -> "*", 254 | Chime.date.years -> "2015-2019" 255 | } 256 | }, 257 | (Throwable|Message msg) { 258 | if (is Throwable msg) { 259 | context.fail(msg, "Cron timer setup"); 260 | context.complete(); 261 | } 262 | else { 263 | if (!msg.body() exists) { 264 | context.fail(Exception("Chime rejects to setup cron timer"), "Cron timer setup"); 265 | context.complete(); 266 | } 267 | } 268 | } 269 | ); 270 | 271 | } 272 | 273 | test shared void unionTimer(AsyncTestContext context) { 274 | 275 | eventBus.consumer ( 276 | union, 277 | timerValidation(union, 1, 3, context) 278 | ); 279 | 280 | eventBus.send ( 281 | chime, 282 | JsonObject { 283 | Chime.key.operation -> Chime.operation.create, 284 | Chime.key.name -> union, 285 | Chime.key.maxCount -> 3, 286 | Chime.key.description -> JsonObject { 287 | Chime.key.type -> Chime.type.union, 288 | Chime.key.timers -> JsonArray { 289 | JsonObject { 290 | Chime.key.type -> Chime.type.cron, 291 | Chime.date.seconds -> "0-59/3", 292 | Chime.date.minutes -> "*", 293 | Chime.date.hours -> "*", 294 | Chime.date.daysOfMonth -> "*", 295 | Chime.date.months -> "*" 296 | }, 297 | JsonObject { 298 | Chime.key.type -> Chime.type.cron, 299 | Chime.date.seconds -> "1-59/3", 300 | Chime.date.minutes -> "*", 301 | Chime.date.hours -> "*", 302 | Chime.date.daysOfMonth -> "*", 303 | Chime.date.months -> "*" 304 | }, 305 | JsonObject { 306 | Chime.key.type -> Chime.type.cron, 307 | Chime.date.seconds -> "2-59/3", 308 | Chime.date.minutes -> "*", 309 | Chime.date.hours -> "*", 310 | Chime.date.daysOfMonth -> "*", 311 | Chime.date.months -> "*" 312 | } 313 | } 314 | } 315 | }, 316 | (Throwable|Message msg) { 317 | if (is Throwable msg) { 318 | context.fail(msg, "Union timer setup"); 319 | context.complete(); 320 | } 321 | else { 322 | if (!msg.body() exists) { 323 | context.fail(Exception("Chime rejects to setup union timer"), "Union timer setup"); 324 | context.complete(); 325 | } 326 | } 327 | } 328 | ); 329 | 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /test/herd/test/schedule/chime/module.ceylon: -------------------------------------------------------------------------------- 1 | import herd.asynctest { 2 | 3 | async 4 | } 5 | 6 | 7 | "_Chime_ unit testing." 8 | since("0.2.0") by("Lis") 9 | native("jvm") async 10 | module herd.test.schedule.chime "0.3.0" { 11 | import herd.schedule.chime "0.3.0"; 12 | shared import ceylon.test "1.3.3.1"; 13 | shared import herd.asynctest "0.7.1"; 14 | shared import io.vertx.ceylon.core "3.4.2"; 15 | } 16 | -------------------------------------------------------------------------------- /test/herd/test/schedule/chime/package.ceylon: -------------------------------------------------------------------------------- 1 | shared package herd.test.schedule.chime; 2 | --------------------------------------------------------------------------------