flycal-cli 0.7.7 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -181
  3. data/bin/flycal +2 -2
  4. data/lib/flycal/cache_annotator.rb +83 -0
  5. data/lib/flycal/cache_key.rb +82 -0
  6. data/lib/flycal/calendar_query.rb +60 -0
  7. data/lib/{flycal_cli → flycal}/calendar_service.rb +48 -4
  8. data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +253 -109
  9. data/lib/{flycal_cli → flycal/cli}/auth.rb +5 -3
  10. data/lib/{flycal_cli → flycal/cli}/clipboard.rb +3 -1
  11. data/lib/{flycal_cli → flycal/cli}/config.rb +4 -2
  12. data/lib/flycal/cli/file_cache.rb +44 -0
  13. data/lib/flycal/cli/locale.rb +58 -0
  14. data/lib/flycal/cli.rb +14 -0
  15. data/lib/flycal/client.rb +186 -0
  16. data/lib/flycal/credentials.rb +34 -0
  17. data/lib/{flycal_cli → flycal}/date_time_parser.rb +4 -4
  18. data/lib/{flycal_cli → flycal}/description_query.rb +3 -3
  19. data/lib/{flycal_cli → flycal}/duration_parser.rb +3 -3
  20. data/lib/flycal/error.rb +5 -0
  21. data/lib/flycal/event_mapper.rb +58 -0
  22. data/lib/{flycal_cli → flycal}/locale.rb +35 -3
  23. data/lib/{flycal_cli → flycal}/mock/calendar_service.rb +1 -1
  24. data/lib/{flycal_cli → flycal}/mock/config.rb +14 -14
  25. data/lib/{flycal_cli → flycal}/mock/event_generator.rb +2 -2
  26. data/lib/flycal/mock.rb +6 -0
  27. data/lib/{flycal_cli → flycal}/pipeline/aggregator.rb +21 -24
  28. data/lib/{flycal_cli → flycal}/pipeline/json_renderer.rb +16 -13
  29. data/lib/{flycal_cli → flycal}/pipeline/params.rb +1 -1
  30. data/lib/{flycal_cli → flycal}/pipeline/renderer.rb +4 -4
  31. data/lib/{flycal_cli → flycal}/pipeline/retriever.rb +1 -1
  32. data/lib/{flycal_cli → flycal}/pipeline/search_pipeline.rb +1 -1
  33. data/lib/{flycal_cli → flycal}/pipeline/text_renderer.rb +9 -8
  34. data/lib/flycal/pipeline.rb +6 -0
  35. data/lib/{flycal_cli → flycal}/slot_finder.rb +1 -1
  36. data/lib/flycal/slot_formatter.rb +195 -0
  37. data/lib/flycal/version.rb +11 -0
  38. data/lib/flycal.rb +12 -0
  39. data/lib/flycal_cli.rb +37 -17
  40. data/mcp/tools.json +21 -8
  41. metadata +36 -25
  42. data/lib/flycal_cli/mock.rb +0 -10
  43. data/lib/flycal_cli/pipeline.rb +0 -14
  44. data/lib/flycal_cli/slot_formatter.rb +0 -68
  45. data/lib/flycal_cli/version.rb +0 -5
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Flycal
6
+ module Cli
7
+ # File-backed cache under ~/.flycal/cache (stores rendered text/json strings).
8
+ class FileCache
9
+ class << self
10
+ def dir
11
+ File.join(Config.config_dir, "cache")
12
+ end
13
+
14
+ def path_for(cache_key)
15
+ File.join(dir, sanitize(cache_key))
16
+ end
17
+
18
+ def read(cache_key, ttl_minutes:)
19
+ path = path_for(cache_key)
20
+ return nil unless File.file?(path)
21
+
22
+ age = Time.now - File.mtime(path)
23
+ return nil if age > ttl_minutes.to_f * 60
24
+
25
+ File.read(path)
26
+ end
27
+
28
+ def write(cache_key, content)
29
+ FileUtils.mkdir_p(dir)
30
+ File.write(path_for(cache_key), content.to_s)
31
+ end
32
+
33
+ def delete(cache_key)
34
+ path = path_for(cache_key)
35
+ File.delete(path) if File.file?(path)
36
+ end
37
+
38
+ def sanitize(cache_key)
39
+ cache_key.to_s.gsub(/[^a-zA-Z0-9._-]/, "_")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flycal
4
+ module Cli
5
+ # CLI-facing locale facade: prefers ~/.flycal config locale, delegates to Flycal::Locale.
6
+ module Locale
7
+ module_function
8
+
9
+ def t(key, vars = {})
10
+ ensure_default_provider!
11
+ Flycal::Locale.t(key, vars)
12
+ end
13
+
14
+ def day_name(date_or_time)
15
+ ensure_default_provider!
16
+ Flycal::Locale.day_name(date_or_time)
17
+ end
18
+
19
+ def day_abbr(date_or_time)
20
+ ensure_default_provider!
21
+ Flycal::Locale.day_abbr(date_or_time)
22
+ end
23
+
24
+ def month_name(date_or_time)
25
+ ensure_default_provider!
26
+ Flycal::Locale.month_name(date_or_time)
27
+ end
28
+
29
+ def format_long_date(date_or_time)
30
+ ensure_default_provider!
31
+ Flycal::Locale.format_long_date(date_or_time)
32
+ end
33
+
34
+ def current_locale
35
+ ensure_default_provider!
36
+ Flycal::Locale.current_locale
37
+ end
38
+
39
+ def override!(locale)
40
+ ensure_default_provider!
41
+ Flycal::Locale.override!(locale)
42
+ end
43
+
44
+ def ensure_default_provider!
45
+ return if Thread.current[:flycal_locale_default_provider]
46
+
47
+ Flycal::Locale.default_locale_provider = lambda {
48
+ begin
49
+ Config.locale
50
+ rescue StandardError
51
+ Flycal::Locale::FALLBACK_LOCALE
52
+ end
53
+ }
54
+ end
55
+ private_class_method :ensure_default_provider!
56
+ end
57
+ end
58
+ end
data/lib/flycal/cli.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flycal"
4
+ require "flycal/cli/config"
5
+ require "flycal/cli/file_cache"
6
+ require "flycal/cli/locale"
7
+ require "flycal/cli/auth"
8
+ require "flycal/cli/clipboard"
9
+ require "flycal/cli/app"
10
+
11
+ module Flycal
12
+ module Cli
13
+ end
14
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+
6
+ module Flycal
7
+ # Entry point for Flycal core. Auth credentials are injected by the caller
8
+ # (FlycalApp / Flycal::Cli), never obtained inside core.
9
+ class Client
10
+ attr_reader :credentials, :calendar
11
+
12
+ def initialize(credentials:)
13
+ raise ArgumentError, "credentials are required" if credentials.nil?
14
+
15
+ @credentials = Credentials.wrap(credentials)
16
+ @calendar = CalendarService.new(@credentials)
17
+ end
18
+
19
+ def list_calendars
20
+ calendar.list_calendars
21
+ end
22
+
23
+ def list_events(calendar_id, time_min:, time_max:, query: nil)
24
+ calendar.list_events(calendar_id, time_min: time_min, time_max: time_max, query: query)
25
+ end
26
+
27
+ def list_all_events(calendar_ids, time_min:, time_max:, query: nil)
28
+ calendar.list_all_events(calendar_ids, time_min: time_min, time_max: time_max, query: query)
29
+ end
30
+
31
+ def create_event(calendar_id: "primary", summary:, start_time:, end_time:, all_day: false)
32
+ calendar.create_event(
33
+ calendar_id: calendar_id,
34
+ summary: summary,
35
+ start_time: start_time,
36
+ end_time: end_time,
37
+ all_day: all_day
38
+ )
39
+ end
40
+
41
+ def update_event(calendar_id: "primary", event_id:, summary:, start_time:, end_time:, all_day: false)
42
+ calendar.update_event(
43
+ calendar_id: calendar_id,
44
+ event_id: event_id,
45
+ summary: summary,
46
+ start_time: start_time,
47
+ end_time: end_time,
48
+ all_day: all_day
49
+ )
50
+ end
51
+
52
+ # Returns Rails/web-friendly event hashes (skips cancelled events).
53
+ def fetch_events(calendar_id:, time_min:, time_max:)
54
+ list_events(calendar_id, time_min: time_min, time_max: time_max)
55
+ .reject { |event| event.status == "cancelled" }
56
+ .filter_map { |event| EventMapper.to_h(event) }
57
+ end
58
+
59
+ def search(params = {})
60
+ bag = params.is_a?(Pipeline::Params) ? params : Pipeline::Params.new(params)
61
+ bag[:format] ||= "json"
62
+ bag[:locale] ||= Locale.current_locale
63
+ Pipeline::SearchPipeline.new(calendar).run(bag)
64
+ end
65
+
66
+ # Resolved slots inputs (defaults applied) suitable for CacheKey.generate.
67
+ # Does not call Google APIs.
68
+ def resolve_slots_query(params = {})
69
+ p = params.respond_to?(:with_indifferent_access) ? params.with_indifferent_access : params
70
+ calendar_ids = Array(p[:calendar_ids]).map(&:to_s).reject(&:empty?)
71
+ raise Flycal::Error, "calendar_ids are required" if calendar_ids.empty?
72
+
73
+ duration_value = p[:duration].presence || "45min"
74
+ in_value = p[:in].presence || "1 week"
75
+ from_value = p[:from].presence || "now"
76
+ free_before_value = p[:free_before].presence || "0m"
77
+ free_after_value = p[:free_after].presence || "0m"
78
+ template_name = p[:template].presence || "work"
79
+ locale = p[:locale].presence || Locale.current_locale
80
+
81
+ slot_duration = DurationParser.to_seconds(duration_value)
82
+ free_before = DurationParser.to_seconds(free_before_value)
83
+ free_after = DurationParser.to_seconds(free_after_value)
84
+ time_min = DateTimeParser.parse(from_value.to_s, locale: locale)
85
+ time_max = DurationParser.add_to_time(in_value, time_min)
86
+ raise Flycal::Error, "'from' must be before end of window" if time_min >= time_max
87
+
88
+ hours = normalize_hours(p[:hours]) || SlotFinder::DEFAULT_HOURS
89
+ days = normalize_days(p[:days]) || SlotFinder::DEFAULT_DAYS
90
+
91
+ {
92
+ command: "slots",
93
+ calendar_ids: calendar_ids,
94
+ duration: duration_value,
95
+ from: from_value,
96
+ in: in_value,
97
+ free_before: free_before_value,
98
+ free_after: free_after_value,
99
+ template: template_name,
100
+ locale: locale,
101
+ calendar: p[:calendar],
102
+ hours: hours,
103
+ days: days,
104
+ time_min: time_min,
105
+ time_max: time_max,
106
+ slot_duration_seconds: slot_duration,
107
+ free_before_seconds: free_before,
108
+ free_after_seconds: free_after,
109
+ format: p[:format].presence || "json"
110
+ }
111
+ end
112
+
113
+ # Find free slots. +params+ keys (string/symbol):
114
+ # calendar_ids (required), duration (e.g. "45min"), from, in,
115
+ # free_before, free_after, hours ([[9,0,18,0]]), days ([1..5]),
116
+ # template (label), locale, calendar (label for meta)
117
+ # Returns a Hash suitable for JSON rendering.
118
+ def slots(params = {})
119
+ p = params.respond_to?(:with_indifferent_access) ? params.with_indifferent_access : params
120
+ q = resolve_slots_query(p)
121
+
122
+ calendars = list_calendars
123
+ calendar_meta = q[:calendar_ids].filter_map do |id|
124
+ cal = calendars.find { |c| c.id == id }
125
+ { id: id, name: cal&.summary || id }
126
+ end
127
+
128
+ fetch_from = q[:time_min] - q[:free_before_seconds]
129
+ events = q[:calendar_ids].flat_map do |calendar_id|
130
+ list_events(calendar_id, time_min: fetch_from, time_max: q[:time_max])
131
+ rescue StandardError => e
132
+ warn "Flycal slots: skip calendar #{calendar_id}: #{e.message}"
133
+ []
134
+ end
135
+
136
+ finder = SlotFinder.new(
137
+ events: events,
138
+ time_min: q[:time_min],
139
+ time_max: q[:time_max],
140
+ slot_duration_seconds: q[:slot_duration_seconds],
141
+ hours: q[:hours],
142
+ days: q[:days],
143
+ free_before_seconds: q[:free_before_seconds],
144
+ free_after_seconds: q[:free_after_seconds]
145
+ )
146
+
147
+ SlotFormatter.payload_hash(
148
+ slots_by_day: finder.slots_by_day,
149
+ time_min: q[:time_min],
150
+ time_max: q[:time_max],
151
+ duration: q[:duration],
152
+ template: q[:template],
153
+ calendars: calendar_meta,
154
+ locale: q[:locale],
155
+ calendar_option: q[:calendar],
156
+ from_option: p[:from],
157
+ in_option: p[:in],
158
+ free_before: q[:free_before],
159
+ free_after: q[:free_after],
160
+ empty_message: Locale.t("slots.no_available")
161
+ )
162
+ end
163
+
164
+ private
165
+
166
+ def normalize_hours(value)
167
+ return nil if value.blank?
168
+ return value if value.is_a?(Array) && value.first.is_a?(Array)
169
+
170
+ # "9:00-18:00" or ["9:00-18:00", "14:00-17:00"]
171
+ Array(value).map do |window|
172
+ start_s, end_s = window.to_s.split("-", 2).map(&:strip)
173
+ sh, sm = start_s.split(":").map(&:to_i)
174
+ eh, em = end_s.split(":").map(&:to_i)
175
+ [ sh, sm, eh, em ]
176
+ end
177
+ end
178
+
179
+ def normalize_days(value)
180
+ return nil if value.blank?
181
+ return value.map(&:to_i) if value.is_a?(Array)
182
+
183
+ value.to_s.split(",").map(&:strip).reject(&:empty?).map(&:to_i)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "googleauth"
4
+
5
+ module Flycal
6
+ # Builds Google auth credentials from externally supplied tokens
7
+ # (Rails OAuth session, CLI token store, etc.).
8
+ class Credentials
9
+ DEFAULT_SCOPE = "https://www.googleapis.com/auth/calendar.readonly"
10
+
11
+ def self.from_tokens(
12
+ access_token:,
13
+ refresh_token: nil,
14
+ client_id:,
15
+ client_secret:,
16
+ expires_at: nil,
17
+ scope: DEFAULT_SCOPE
18
+ )
19
+ opts = {
20
+ client_id: client_id,
21
+ client_secret: client_secret,
22
+ scope: scope,
23
+ access_token: access_token,
24
+ refresh_token: refresh_token
25
+ }
26
+ opts[:expires_at] = expires_at.to_i if expires_at
27
+ Google::Auth::UserRefreshCredentials.new(**opts)
28
+ end
29
+
30
+ def self.wrap(credentials)
31
+ credentials
32
+ end
33
+ end
34
+ end
@@ -5,7 +5,7 @@ require "time"
5
5
  require "active_support/core_ext/time"
6
6
  require "active_support/core_ext/date"
7
7
 
8
- module FlycalCli
8
+ module Flycal
9
9
  # Parses CLI date/time arguments in a locale-aware way.
10
10
  #
11
11
  # Always accepts ISO-like forms:
@@ -39,7 +39,7 @@ module FlycalCli
39
39
  class << self
40
40
  def parse(str, end_of_day: false, locale: nil)
41
41
  value = str.to_s.strip
42
- raise FlycalCli::Error, invalid_message(str) if value.empty?
42
+ raise Flycal::Error, invalid_message(str) if value.empty?
43
43
 
44
44
  relative = parse_relative(value, end_of_day: end_of_day)
45
45
  return relative if relative
@@ -65,7 +65,7 @@ module FlycalCli
65
65
  parsed = Time.parse(value)
66
66
  return end_of_day && !time_component?(value) ? end_of_day_for(parsed.to_date) : parsed
67
67
  rescue ArgumentError
68
- raise FlycalCli::Error, invalid_message(str)
68
+ raise Flycal::Error, invalid_message(str)
69
69
  end
70
70
  end
71
71
 
@@ -194,7 +194,7 @@ module FlycalCli
194
194
 
195
195
  Time.local(year, month, day, hour, min || 0, sec || 0)
196
196
  rescue ArgumentError
197
- raise FlycalCli::Error, "Invalid date: #{year}-#{month}-#{day}"
197
+ raise Flycal::Error, "Invalid date: #{year}-#{month}-#{day}"
198
198
  end
199
199
 
200
200
  def end_of_day_for(date)
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
4
- # Shared helpers for --description filtering and --groupBy description.
3
+ module Flycal
4
+ # Shared helpers for --description filtering and --groupBy string patterns.
5
5
  #
6
- # OR terms are separated by "|" (also "," for grouping convenience).
6
+ # OR / group terms are separated by "|" (also ",").
7
7
  # Matching is case-insensitive against event summary and description.
8
8
  module DescriptionQuery
9
9
  module_function
@@ -3,7 +3,7 @@
3
3
  require "active_support/core_ext/numeric/time"
4
4
  require "active_support/core_ext/integer/time"
5
5
 
6
- module FlycalCli
6
+ module Flycal
7
7
  class DurationParser
8
8
  UNITS = {
9
9
  "s" => :seconds, "sec" => :seconds, "second" => :seconds, "seconds" => :seconds,
@@ -20,12 +20,12 @@ module FlycalCli
20
20
  normalized = str.to_s.strip.downcase
21
21
  match = normalized.match(/\A(\d+(?:\.\d+)?)\s*([a-z]+)\z/) ||
22
22
  normalized.match(/\A(\d+(?:\.\d+)?)([a-z]+)\z/)
23
- raise FlycalCli::Error, invalid_message(str) unless match
23
+ raise Flycal::Error, invalid_message(str) unless match
24
24
 
25
25
  value = match[1].to_f
26
26
  unit_key = match[2]
27
27
  unit = UNITS[unit_key]
28
- raise FlycalCli::Error, invalid_message(str) unless unit
28
+ raise Flycal::Error, invalid_message(str) unless unit
29
29
  # Ambiguous bare "m": treat as minutes (not months)
30
30
  unit = :minutes if unit_key == "m"
31
31
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flycal
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Flycal
6
+ # Normalizes Google Calendar API event objects into plain hashes.
7
+ module EventMapper
8
+ module_function
9
+
10
+ def to_h(event)
11
+ start_time = extract_start(event)
12
+ end_time = extract_end(event, start_time)
13
+
14
+ {
15
+ id: event.id || SecureRandom.uuid,
16
+ summary: event.summary || "(No title)",
17
+ description: event.description,
18
+ location: event.location,
19
+ start_time: start_time,
20
+ end_time: end_time,
21
+ all_day: !event.start&.date.nil? && event.start&.date_time.nil?,
22
+ status: event.status || "confirmed",
23
+ html_link: event.html_link,
24
+ creator: event.creator&.email,
25
+ organizer: event.organizer&.email,
26
+ attendees: Array(event.attendees).map do |attendee|
27
+ { email: attendee.email, response_status: attendee.response_status }
28
+ end
29
+ }
30
+ rescue StandardError
31
+ nil
32
+ end
33
+
34
+ def extract_start(event)
35
+ if event.start&.date_time
36
+ event.start.date_time
37
+ elsif event.start&.date
38
+ Date.parse(event.start.date.to_s)
39
+ else
40
+ Time.now
41
+ end
42
+ rescue ArgumentError
43
+ Time.now
44
+ end
45
+
46
+ def extract_end(event, start_time)
47
+ if event.end&.date_time
48
+ event.end.date_time
49
+ elsif event.end&.date
50
+ Date.parse(event.end.date.to_s)
51
+ else
52
+ start_time + 3600
53
+ end
54
+ rescue ArgumentError
55
+ start_time + 3600
56
+ end
57
+ end
58
+ end
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "json"
4
4
 
5
- module FlycalCli
5
+ module Flycal
6
+ # Shared i18n helpers used by core formatters/pipelines.
7
+ # Locale JSON files live with the CLI package (flycal_cli/locales) by default.
6
8
  module Locale
7
9
  module_function
8
10
 
@@ -38,20 +40,50 @@ module FlycalCli
38
40
  end
39
41
 
40
42
  def current_locale
41
- Thread.current[:flycal_locale_override] || Config.locale
43
+ Thread.current[:flycal_locale_override] || default_locale
44
+ end
45
+
46
+ def default_locale
47
+ provider = Thread.current[:flycal_locale_default_provider]
48
+ value = provider.call if provider.respond_to?(:call)
49
+ value = value.to_s.strip if value
50
+ value && !value.empty? ? value : FALLBACK_LOCALE
42
51
  rescue StandardError
43
52
  FALLBACK_LOCALE
44
53
  end
45
54
 
55
+ def default_locale_provider=(provider)
56
+ Thread.current[:flycal_locale_default_provider] = provider
57
+ end
58
+
46
59
  def override!(locale)
47
60
  Thread.current[:flycal_locale_override] = locale.to_s if locale && !locale.to_s.strip.empty?
48
61
  end
49
62
 
63
+ def locales_path
64
+ Thread.current[:flycal_locales_path] || default_locales_path
65
+ end
66
+
67
+ def locales_path=(path)
68
+ Thread.current[:flycal_locales_path] = path
69
+ @cache = nil
70
+ end
71
+
72
+ def default_locales_path
73
+ # Monorepo: flycal_cli/locales; gem package: lib/../locales
74
+ candidates = [
75
+ File.expand_path("../../flycal_cli/locales", __dir__),
76
+ File.expand_path("../../../locales", __dir__),
77
+ File.expand_path("../../locales", __dir__)
78
+ ]
79
+ candidates.find { |path| Dir.exist?(path) } || candidates.first
80
+ end
81
+
50
82
  def translations(locale)
51
83
  @cache ||= {}
52
84
  loc = locale.to_s
53
85
  @cache[loc] ||= begin
54
- path = File.expand_path("../../locales/#{loc}.json", __dir__)
86
+ path = File.join(locales_path, "#{loc}.json")
55
87
  File.exist?(path) ? JSON.parse(File.read(path)) : {}
56
88
  end
57
89
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
3
+ module Flycal
4
4
  module Mock
5
5
  # Duck-typed CalendarService backed by generated mock events (no Google API).
6
6
  class CalendarService
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "json"
4
4
 
5
- module FlycalCli
5
+ module Flycal
6
6
  module Mock
7
7
  # Resolves mock options from CLI and/or a JSON template under mocks/.
8
8
  class Config
@@ -45,7 +45,7 @@ module FlycalCli
45
45
  calendar = merged["mockCalendar"].to_s
46
46
  calendar = options[:mockCalendar].to_s if calendar.empty?
47
47
  @calendar_name = calendar
48
- raise FlycalCli::Error,
48
+ raise Flycal::Error,
49
49
  "Mock mode requires mockCalendar in the template (or --mockCalendar)." if @calendar_name.empty?
50
50
 
51
51
  @raw = merged
@@ -84,14 +84,14 @@ module FlycalCli
84
84
  return {} if name.to_s.empty?
85
85
 
86
86
  path = find_template_path(name)
87
- raise FlycalCli::Error, "Mock template not found: #{name.inspect} (looked in ./mocks, ./mockTemplates, and gem paths)." unless path
87
+ raise Flycal::Error, "Mock template not found: #{name.inspect} (looked in ./mocks, ./mockTemplates, and gem paths)." unless path
88
88
 
89
89
  data = JSON.parse(File.read(path))
90
- raise FlycalCli::Error, "Mock template #{name.inspect} must be a JSON object." unless data.is_a?(Hash)
90
+ raise Flycal::Error, "Mock template #{name.inspect} must be a JSON object." unless data.is_a?(Hash)
91
91
 
92
92
  data
93
93
  rescue JSON::ParserError => e
94
- raise FlycalCli::Error, "Invalid mock template JSON (#{name}): #{e.message}"
94
+ raise Flycal::Error, "Invalid mock template JSON (#{name}): #{e.message}"
95
95
  end
96
96
 
97
97
  def find_template_path(name)
@@ -108,13 +108,13 @@ module FlycalCli
108
108
 
109
109
  def split_patterns(value)
110
110
  list = value.to_s.split(",").map(&:strip).reject(&:empty?)
111
- raise FlycalCli::Error, "mockEventDescriptionPatterns must list at least one pattern." if list.empty?
111
+ raise Flycal::Error, "mockEventDescriptionPatterns must list at least one pattern." if list.empty?
112
112
 
113
113
  list
114
114
  end
115
115
 
116
116
  def parse_date(value, end_of_day:)
117
- raise FlycalCli::Error, "Missing mock date." if value.to_s.empty?
117
+ raise Flycal::Error, "Missing mock date." if value.to_s.empty?
118
118
 
119
119
  DateTimeParser.parse(value.to_s, end_of_day: end_of_day)
120
120
  end
@@ -122,12 +122,12 @@ module FlycalCli
122
122
  def parse_hour_minute(value)
123
123
  str = value.to_s.strip
124
124
  match = str.match(/\A(\d{1,2}):(\d{2})\z/)
125
- raise FlycalCli::Error, "Invalid mock hour #{value.inspect}. Use HH:MM (e.g. 09:00)." unless match
125
+ raise Flycal::Error, "Invalid mock hour #{value.inspect}. Use HH:MM (e.g. 09:00)." unless match
126
126
 
127
127
  hour = match[1].to_i
128
128
  min = match[2].to_i
129
129
  unless hour.between?(0, 23) && min.between?(0, 59)
130
- raise FlycalCli::Error, "Invalid mock hour #{value.inspect}. Use HH:MM (e.g. 09:00)."
130
+ raise Flycal::Error, "Invalid mock hour #{value.inspect}. Use HH:MM (e.g. 09:00)."
131
131
  end
132
132
 
133
133
  [hour, min]
@@ -147,22 +147,22 @@ module FlycalCli
147
147
  missing << "mockEventTo" if range_to.nil?
148
148
  missing << "mockEventDurationMin" if duration_min_seconds <= 0
149
149
  missing << "mockEventDurationMax" if duration_max_seconds <= 0
150
- raise FlycalCli::Error, "Missing mock parameters: #{missing.join(", ")}" unless missing.empty?
150
+ raise Flycal::Error, "Missing mock parameters: #{missing.join(", ")}" unless missing.empty?
151
151
 
152
152
  if duration_min_seconds > duration_max_seconds
153
- raise FlycalCli::Error, "mockEventDurationMin must be <= mockEventDurationMax."
153
+ raise Flycal::Error, "mockEventDurationMin must be <= mockEventDurationMax."
154
154
  end
155
155
  if range_from > range_to
156
- raise FlycalCli::Error, "mockEventFrom must be before mockEventTo."
156
+ raise Flycal::Error, "mockEventFrom must be before mockEventTo."
157
157
  end
158
158
 
159
159
  from_mins = hours_from[0] * 60 + hours_from[1]
160
160
  to_mins = hours_to[0] * 60 + hours_to[1]
161
161
  if from_mins >= to_mins
162
- raise FlycalCli::Error, "mockEventHoursFrom must be before mockEventHoursTo."
162
+ raise Flycal::Error, "mockEventHoursFrom must be before mockEventHoursTo."
163
163
  end
164
164
  if duration_min_seconds > (to_mins - from_mins) * 60
165
- raise FlycalCli::Error,
165
+ raise Flycal::Error,
166
166
  "mockEventDurationMin does not fit in mockEventHoursFrom..mockEventHoursTo window."
167
167
  end
168
168
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
3
+ module Flycal
4
4
  module Mock
5
5
  # Builds deterministic fake calendar events from a Mock::Config.
6
6
  class EventGenerator
@@ -60,7 +60,7 @@ module FlycalCli
60
60
  window_end = day.to_time + (to_h * 3600) + (to_m * 60)
61
61
  latest_start = window_end - duration_seconds
62
62
  if latest_start < window_start
63
- raise FlycalCli::Error, "Mock event duration does not fit in the daily hours window."
63
+ raise Flycal::Error, "Mock event duration does not fit in the daily hours window."
64
64
  end
65
65
 
66
66
  span = (latest_start - window_start).to_i
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flycal
4
+ module Mock
5
+ end
6
+ end