moco-ruby 0.1.2 → 1.0.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.
data/lib/moco/sync.rb CHANGED
@@ -1,105 +1,187 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fuzzy_match"
4
- require_relative "api"
4
+ require_relative "client"
5
5
 
6
6
  module MOCO
7
7
  # Match and map projects and tasks between MOCO instances and sync activities
8
8
  class Sync
9
9
  attr_reader :project_mapping, :task_mapping, :source_projects, :target_projects
10
- attr_accessor :project_match_threshold, :task_match_threshold, :dry_run
11
-
12
- def initialize(source_instance_api, target_instance_api, **args)
13
- @source_api = source_instance_api
14
- @target_api = target_instance_api
10
+ attr_accessor :project_match_threshold, :task_match_threshold, :dry_run, :debug
11
+
12
+ def initialize(source_client, target_client, **args)
13
+ @source = source_client
14
+ @target = target_client
15
15
  @project_match_threshold = args.fetch(:project_match_threshold, 0.8)
16
16
  @task_match_threshold = args.fetch(:task_match_threshold, 0.45)
17
17
  @filters = args.fetch(:filters, {})
18
18
  @dry_run = args.fetch(:dry_run, false)
19
-
19
+ @debug = args.fetch(:debug, false)
20
+
20
21
  @project_mapping = {}
21
22
  @task_mapping = {}
22
-
23
+
23
24
  fetch_assigned_projects
24
25
  build_initial_mappings
25
26
  end
26
-
27
- # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
27
+
28
+ # rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
28
29
  def sync(&callbacks)
29
30
  results = []
30
31
 
31
- source_activities_r = @source_api.get_activities(@filters.fetch(:source, {}))
32
- target_activities_r = @target_api.get_activities(@filters.fetch(:target, {}))
32
+ source_activity_filters = @filters.fetch(:source, {})
33
+ source_activities_r = @source.activities.where(source_activity_filters).all
34
+ debug_log "Fetched #{source_activities_r.size} source activities"
35
+
36
+ # Log source activities for debugging
37
+ debug_log "Source activities:"
38
+ source_activities_r.each do |activity|
39
+ debug_log " Source Activity: #{activity.id}, Date: #{activity.date}, Project: #{activity.project&.id} (#{activity.project&.name}), Task: #{activity.task&.id} (#{activity.task&.name}), Hours: #{activity.hours}, Description: #{activity.description}, Remote ID: #{activity.remote_id}"
33
40
 
41
+ # Also log the expected target activity for each source activity
42
+ begin
43
+ expected = get_expected_target_activity(activity)
44
+ if expected
45
+ project_id = expected.project&.id rescue "N/A"
46
+ task_id = expected.task&.id rescue "N/A"
47
+ remote_id = expected.instance_variable_get(:@attributes)[:remote_id] rescue "N/A"
48
+ debug_log " Expected Target: Project: #{project_id}, Task: #{task_id}, Remote ID: #{remote_id}"
49
+ end
50
+ rescue => e
51
+ debug_log " Error getting expected target: #{e.message}"
52
+ end
53
+ end
54
+
55
+ target_activity_filters = @filters.fetch(:target, {})
56
+ target_activities_r = @target.activities.where(target_activity_filters).all
57
+ debug_log "Fetched #{target_activities_r.size} target activities"
58
+
59
+ # Log target activities for debugging
60
+ debug_log "Target activities:"
61
+ target_activities_r.each do |activity|
62
+ debug_log " Target Activity: #{activity.id}, Date: #{activity.date}, Project: #{activity.project&.id} (#{activity.project&.name}), Task: #{activity.task&.id} (#{activity.task&.name}), Hours: #{activity.hours}, Description: #{activity.description}, Remote ID: #{activity.remote_id}"
63
+ end
64
+
65
+ # Group activities by date and then by project_id for consistent lookups
34
66
  source_activities_grouped = source_activities_r.group_by(&:date).transform_values do |activities|
35
- activities.group_by(&:project)
67
+ activities.group_by { |a| a.project&.id } # Group by project ID
36
68
  end
37
69
  target_activities_grouped = target_activities_r.group_by(&:date).transform_values do |activities|
38
- activities.group_by(&:project)
70
+ activities.group_by { |a| a.project&.id } # Group by project ID
39
71
  end
40
72
 
41
73
  used_source_activities = []
42
74
  used_target_activities = []
43
75
 
44
- source_activities_grouped.each do |date, activities_by_project|
45
- activities_by_project.each do |project, source_activities|
46
- target_activities = target_activities_grouped.fetch(date, {}).fetch(@project_mapping[project.id], [])
47
- next if source_activities.empty? || target_activities.empty?
76
+ debug_log "Starting main sync loop..."
77
+ source_activities_grouped.each do |date, activities_by_project_id|
78
+ debug_log "Processing date: #{date}"
79
+ activities_by_project_id.each do |source_project_id, source_activities|
80
+ debug_log " Processing source project ID: #{source_project_id} (#{source_activities.count} activities)"
81
+ # Find the corresponding target project ID using the mapping
82
+ target_project_object = @project_mapping[source_project_id]
83
+ unless target_project_object
84
+ debug_log " Skipping - Source project ID #{source_project_id} not mapped."
85
+ next
86
+ end
87
+
88
+ target_project_id = target_project_object.id
89
+ # Fetch target activities using the target project ID
90
+ target_activities = target_activities_grouped.fetch(date, {}).fetch(target_project_id, [])
91
+ debug_log " Found #{target_activities.count} target activities for target project ID: #{target_project_id}"
92
+
93
+ if source_activities.empty? || target_activities.empty?
94
+ debug_log " Skipping - No source or target activities for this date/project pair."
95
+ next
96
+ end
48
97
 
49
98
  matches = calculate_matches(source_activities, target_activities)
99
+ debug_log " Calculated #{matches.count} potential matches."
50
100
  matches.sort_by! { |match| -match[:score] }
51
101
 
102
+ debug_log " Entering matches loop..."
52
103
  matches.each do |match|
53
104
  source_activity, target_activity = match[:activity]
54
105
  score = match[:score]
106
+ debug_log " Match Pair: Score=#{score}, Source=#{source_activity.id}, Target=#{target_activity.id}"
55
107
 
56
- next if used_source_activities.include?(source_activity) || used_target_activities.include?(target_activity)
108
+ if used_source_activities.include?(source_activity) || used_target_activities.include?(target_activity)
109
+ debug_log " Skipping match pair - already used: Source used=#{used_source_activities.include?(source_activity)}, Target used=#{used_target_activities.include?(target_activity)}"
110
+ next
111
+ end
57
112
 
58
- best_score = score
113
+ best_score = score # Since we sorted, this is the best score for this unused pair
59
114
  best_match = target_activity
60
115
  expected_target_activity = get_expected_target_activity(source_activity)
116
+ debug_log " Processing best score #{best_score} for Source=#{source_activity.id}"
61
117
 
62
118
  case best_score
63
119
  when 100
120
+ debug_log " Case 100: Equal"
64
121
  # 100 - perfect match found, nothing needs doing
65
122
  callbacks&.call(:equal, source_activity, expected_target_activity)
123
+ # Mark both as used
124
+ debug_log " Marking Source=#{source_activity.id} and Target=#{target_activity.id} as used."
125
+ used_source_activities << source_activity
126
+ used_target_activities << target_activity
66
127
  when 60...100
128
+ debug_log " Case 60-99: Update"
67
129
  # >=60 <100 - match with some differences
68
130
  expected_target_activity.to_h.except(:id, :user, :customer).each do |k, v|
131
+ debug_log " Updating attribute #{k} on Target=#{target_activity.id}"
69
132
  best_match.send("#{k}=", v)
70
133
  end
71
134
  callbacks&.call(:update, source_activity, best_match)
72
135
  unless @dry_run
73
- results << @target_api.update_activity(best_match)
136
+ debug_log " Executing API update for Target=#{target_activity.id}"
137
+ results << @target.activities.update(best_match.id, best_match.attributes) # Pass ID and attributes
74
138
  callbacks&.call(:updated, source_activity, best_match, results.last)
75
139
  end
140
+ # Mark both as used
141
+ debug_log " Marking Source=#{source_activity.id} and Target=#{target_activity.id} as used."
142
+ used_source_activities << source_activity
143
+ used_target_activities << target_activity
76
144
  when 0...60
77
- # <60 - no good match found, create new entry
78
- callbacks&.call(:create, source_activity, expected_target_activity)
79
- unless @dry_run
80
- results << @target_api.create_activity(expected_target_activity)
81
- callbacks&.call(:created, source_activity, best_match, results.last)
82
- end
145
+ debug_log " Case 0-59: Low score, doing nothing for this pair."
146
+ # <60 - Low score for this specific pair. Do nothing here.
147
+ # Creation is handled later if source_activity remains unused.
148
+ nil # Explicitly do nothing
83
149
  end
84
-
85
- used_source_activities << source_activity
86
- used_target_activities << target_activity
150
+ # Only mark activities as used if score >= 60 (handled within the case branches above)
87
151
  end
152
+ debug_log " Finished matches loop."
88
153
  end
154
+ debug_log " Finished processing project IDs for date #{date}."
89
155
  end
156
+ debug_log "Finished main sync loop."
90
157
 
158
+ # Second loop: Create source activities that were never used (i.e., had no match >= 60)
159
+ debug_log "Starting creation loop..."
91
160
  source_activities_r.each do |source_activity|
92
- unless used_source_activities.include?(source_activity)
93
- next unless @project_mapping[source_activity.project.id]
94
-
95
- expected_target_activity = get_expected_target_activity(source_activity)
96
- callbacks&.call(:create, source_activity, expected_target_activity)
97
- unless @dry_run
98
- results << @target_api.create_activity(expected_target_activity)
99
- callbacks&.call(:created, source_activity, expected_target_activity, results.last)
100
- end
161
+ if used_source_activities.include?(source_activity)
162
+ debug_log " Skipping creation for Source=#{source_activity.id} - already used."
163
+ next
101
164
  end
165
+ # Use safe navigation in case project is nil
166
+ source_project_id = source_activity.project&.id
167
+ unless @project_mapping[source_project_id]
168
+ debug_log " Skipping creation for Source=#{source_activity.id} - project #{source_project_id} not mapped."
169
+ next
170
+ end
171
+
172
+ debug_log " Processing creation for Source=#{source_activity.id}"
173
+ expected_target_activity = get_expected_target_activity(source_activity)
174
+ callbacks&.call(:create, source_activity, expected_target_activity)
175
+ next if @dry_run
176
+
177
+ debug_log " Executing API create."
178
+ # Pass attributes hash to create
179
+ created_activity = @target.activities.create(expected_target_activity.attributes)
180
+ results << created_activity
181
+ # Pass the actual created activity object to the callback
182
+ callbacks&.call(:created, source_activity, created_activity, results.last)
102
183
  end
184
+ debug_log "Finished creation loop."
103
185
 
104
186
  results
105
187
  end
@@ -107,21 +189,54 @@ module MOCO
107
189
 
108
190
  private
109
191
 
192
+ def debug_log(message)
193
+ warn "[SYNC DEBUG] #{message}" if @debug
194
+ end
195
+
110
196
  def get_expected_target_activity(source_activity)
111
- source_activity.dup.tap do |a|
112
- a.task = @task_mapping[source_activity.task.id]
113
- a.project = @project_mapping[source_activity.project.id]
114
- end
197
+ # Create a duplicate of the source activity
198
+ new_activity = source_activity.dup
199
+
200
+ # Get the attributes hash
201
+ attrs = new_activity.instance_variable_get(:@attributes)
202
+
203
+ # Store the mapped task and project objects for reference
204
+ mapped_task = @task_mapping[source_activity.task&.id]
205
+ mapped_project = @project_mapping[source_activity.project&.id]
206
+
207
+ # Set the task_id and project_id attributes instead of the full objects
208
+ attrs[:task_id] = mapped_task.id if mapped_task
209
+ attrs[:project_id] = mapped_project.id if mapped_project
210
+
211
+ # Set remote_id to the source activity ID for future matching
212
+ attrs[:remote_id] = source_activity.id.to_s
213
+
214
+ # Remove the full objects from the attributes hash
215
+ attrs.delete(:task)
216
+ attrs.delete(:project)
217
+
218
+ # Return the modified activity
219
+ new_activity
115
220
  end
116
221
 
117
222
  def calculate_matches(source_activities, target_activities)
118
223
  matches = []
224
+
119
225
  source_activities.each do |source_activity|
120
226
  target_activities.each do |target_activity|
121
- score = score_activity_match(get_expected_target_activity(source_activity), target_activity)
122
- matches << { activity: [source_activity, target_activity], score: score }
227
+ # First check if this is a previously synced activity by comparing IDs directly
228
+ if target_activity.respond_to?(:remote_id) &&
229
+ target_activity.remote_id.to_s == source_activity.id.to_s
230
+ debug_log "Direct match found: target.remote_id=#{target_activity.remote_id} matches source.id=#{source_activity.id}" if @debug
231
+ matches << { activity: [source_activity, target_activity], score: 100 }
232
+ else
233
+ # If no direct match, use the regular scoring method
234
+ score = score_activity_match(get_expected_target_activity(source_activity), target_activity)
235
+ matches << { activity: [source_activity, target_activity], score: }
236
+ end
123
237
  end
124
238
  end
239
+
125
240
  matches
126
241
  end
127
242
 
@@ -133,27 +248,145 @@ module MOCO
133
248
  [0.0, score].max
134
249
  end
135
250
 
136
- # rubocop:disable Metrics/AbcSize
251
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
137
252
  def score_activity_match(a, b)
253
+ # Must be same project
138
254
  return 0 if a.project != b.project
139
255
 
256
+ # Check for exact ID match (for activities that were previously synced)
257
+ # This is the most important check and overrides all others
258
+ if a.id.to_s == b.remote_id.to_s || b.id.to_s == a.remote_id.to_s
259
+ debug_log "Found exact ID match between #{a.id} and #{b.id}" if @debug
260
+ return 100
261
+ end
262
+
263
+ # Check for exact ID match in remote_id field
264
+ if a.remote_id.to_s == b.id.to_s || b.remote_id.to_s == a.id.to_s
265
+ debug_log "Found exact ID match in remote_id: a.remote_id=#{a.remote_id}, b.id=#{b.id}" if @debug
266
+ return 100
267
+ end
268
+
269
+ # Additional check for remote_id in attributes hash
270
+ begin
271
+ a_remote_id = a.instance_variable_get(:@attributes)[:remote_id].to_s rescue nil
272
+ b_remote_id = b.instance_variable_get(:@attributes)[:remote_id].to_s rescue nil
273
+
274
+ if (a_remote_id && !a_remote_id.empty? && a_remote_id == b.id.to_s) ||
275
+ (b_remote_id && !b_remote_id.empty? && b_remote_id == a.id.to_s)
276
+ debug_log "Found exact ID match in attributes hash: a.attributes[:remote_id]=#{a_remote_id}, b.id=#{b.id}" if @debug
277
+ return 100
278
+ end
279
+ rescue => e
280
+ debug_log "Error checking remote_id in attributes: #{e.message}" if @debug
281
+ end
282
+
283
+ # Date comparison - must be same date
284
+ # Convert to string for comparison to handle different date object types
285
+ # and normalize format to YYYY-MM-DD
286
+ debug_log "Raw dates: a.date=#{a.date.inspect} (#{a.date.class}), b.date=#{b.date.inspect} (#{b.date.class})" if @debug
287
+
288
+ # Normalize dates to YYYY-MM-DD format
289
+ a_date = normalize_date(a.date)
290
+ b_date = normalize_date(b.date)
291
+
292
+ debug_log "Normalized dates: a_date=#{a_date}, b_date=#{b_date}" if @debug
293
+
294
+ if a_date != b_date
295
+ debug_log "Date mismatch: #{a_date} vs #{b_date}" if @debug
296
+ return 0
297
+ end
298
+
140
299
  score = 0
141
- # (mapped) task is the same as the source task
142
- score += 20 if a.task == b.task
143
- # description fuzzy match score (0.0 .. 1.0)
144
- _, description_match_score = FuzzyMatch.new([a.description]).find_with_score(b.description)
145
- score += (description_match_score * 40.0).to_i if description_match_score
146
- # differences in time tracked are weighted by sqrt of diff clamped to 7h
147
- # i.e. smaller differences are worth higher scores; 1.75h diff = 0.5 score * 40
148
- score += (clamped_factored_diff_score(a.hours, b.hours) * 40.0).to_i
300
+
301
+ # Task matching is important (30 points)
302
+ if a.task&.id == b.task&.id
303
+ score += 30
304
+ debug_log "Task match: +30 points" if @debug
305
+ end
306
+
307
+ # Description matching (up to 30 points)
308
+ if a.description.to_s.strip.empty? && b.description.to_s.strip.empty?
309
+ # Both empty descriptions - consider it a match for this attribute
310
+ score += 30
311
+ debug_log "Empty description match: +30 points" if @debug
312
+ else
313
+ # Use fuzzy matching for non-empty descriptions
314
+ _, description_match_score = FuzzyMatch.new([a.description.to_s]).find_with_score(b.description.to_s)
315
+ if description_match_score
316
+ desc_points = (description_match_score * 30.0).to_i
317
+ score += desc_points
318
+ debug_log "Description match (#{description_match_score}): +#{desc_points} points" if @debug
319
+ end
320
+ end
321
+
322
+ # Hours matching (up to 40 points)
323
+ # Exact hour match gets full points
324
+ if a.hours == b.hours
325
+ score += 40
326
+ debug_log "Exact hours match: +40 points" if @debug
327
+ else
328
+ # Otherwise use the clamped difference score
329
+ hours_points = (clamped_factored_diff_score(a.hours, b.hours) * 40.0).to_i
330
+ score += hours_points
331
+ debug_log "Hours similarity (#{a.hours} vs #{b.hours}): +#{hours_points} points" if @debug
332
+ end
333
+
334
+ debug_log "Final score for #{a.id} vs #{b.id}: #{score}" if @debug
149
335
 
150
336
  score
151
337
  end
152
- # rubocop:enable Metrics/AbcSize
338
+
339
+ # Helper method to normalize dates to YYYY-MM-DD format
340
+ def normalize_date(date_value)
341
+ return nil if date_value.nil?
342
+
343
+ date_str = date_value.to_s
344
+
345
+ # First try to extract YYYY-MM-DD from ISO format
346
+ date_str = date_str.split("T").first.strip if date_str.include?("T")
347
+
348
+ # Handle different date formats
349
+ begin
350
+ # Try to parse as Date object if it's not already in YYYY-MM-DD format
351
+ date_str = Date.parse(date_str).strftime("%Y-%m-%d") unless date_str =~ /^\d{4}-\d{2}-\d{2}$/
352
+ rescue StandardError => e
353
+ debug_log "Error normalizing date '#{date_str}': #{e.message}" if @debug
354
+ # If parsing fails, return the original string
355
+ end
356
+
357
+ date_str
358
+ end
359
+
360
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
153
361
 
154
362
  def fetch_assigned_projects
155
- @source_projects = @source_api.get_assigned_projects(**@filters.fetch(:source, {}).merge(active: "true"))
156
- @target_projects = @target_api.get_assigned_projects(**@filters.fetch(:target, {}).merge(active: "true"))
363
+ # Use .projects.assigned for the source, standard .projects for the target
364
+ source_filters = @filters.fetch(:source, {}).merge(active: "true")
365
+ # Get the proxy, then fetch all results into the instance variable
366
+ @source_projects = @source.projects.assigned.where(source_filters).all
367
+ debug_log "Found #{@source_projects.size} source projects:"
368
+ @source_projects.each do |project|
369
+ debug_log " Source Project: #{project.id} - #{project.name} (#{project.identifier})"
370
+ debug_log " Tasks:"
371
+ project.tasks.each do |task|
372
+ debug_log " Task: #{task.id} - #{task.name}"
373
+ end
374
+ end
375
+
376
+ target_filters = @filters.fetch(:target, {}).merge(active: "true")
377
+ # Get the proxy, then fetch all results into the instance variable
378
+ @target_projects = @target.projects.where(target_filters).all
379
+ debug_log "Found #{@target_projects.size} target projects:"
380
+ @target_projects.each do |project|
381
+ debug_log " Target Project: #{project.id} - #{project.name} (#{project.identifier})"
382
+ debug_log " Tasks:"
383
+ project.tasks.each do |task|
384
+ debug_log " Task: #{task.id} - #{task.name}"
385
+ end
386
+ end
387
+
388
+ # NOTE: The @source_projects and @target_projects are now Arrays of entities,
389
+ # not CollectionProxy or EntityCollection objects.
157
390
  end
158
391
 
159
392
  def build_initial_mappings
@@ -162,21 +395,63 @@ module MOCO
162
395
  next unless source_project
163
396
 
164
397
  @project_mapping[source_project.id] = target_project
398
+ debug_log "Mapped source project #{source_project.id} (#{source_project.name}) to target project #{target_project.id} (#{target_project.name})"
399
+
165
400
  target_project.tasks.each do |target_task|
166
401
  source_task = match_task(target_task, source_project)
167
- @task_mapping[source_task.id] = target_task if source_task
402
+ if source_task
403
+ @task_mapping[source_task.id] = target_task
404
+ debug_log " Mapped source task #{source_task.id} (#{source_task.name}) to target task #{target_task.id} (#{target_task.name})"
405
+ else
406
+ debug_log " No matching source task found for target task #{target_task.id} (#{target_task.name})"
407
+ end
168
408
  end
169
409
  end
410
+
411
+ # Log the final mappings
412
+ debug_log "Final project mappings:"
413
+ @project_mapping.each do |source_id, target_project|
414
+ debug_log " Source project #{source_id} -> Target project #{target_project.id} (#{target_project.name})"
415
+ end
416
+
417
+ debug_log "Final task mappings:"
418
+ @task_mapping.each do |source_id, target_task|
419
+ debug_log " Source task #{source_id} -> Target task #{target_task.id} (#{target_task.name})"
420
+ end
170
421
  end
171
422
 
172
423
  def match_project(target_project)
173
- matcher = FuzzyMatch.new(@source_projects, read: :name)
174
- matcher.find(target_project.name, threshold: @project_match_threshold)
424
+ # Create array of search objects manually since we can't call map on EntityCollection
425
+ searchable_projects = []
426
+
427
+ # Manually iterate since we can't rely on Enumerable methods
428
+ @source_projects.each do |project|
429
+ debug_log "Checking source project: #{project.inspect}" if @debug
430
+ searchable_projects << { original: project, name: project.name }
431
+ end
432
+
433
+ matcher = FuzzyMatch.new(searchable_projects, read: :name)
434
+ match = matcher.find(target_project.name, threshold: @project_match_threshold)
435
+ match[:original] if match
175
436
  end
176
437
 
177
438
  def match_task(target_task, source_project)
178
- matcher = FuzzyMatch.new(source_project.tasks, read: :name)
179
- matcher.find(target_task.name, threshold: @task_match_threshold)
439
+ # Get tasks from the source project
440
+ tasks = source_project.tasks
441
+
442
+ # Create array of search objects manually since we can't rely on Enumerable methods
443
+
444
+ # Manually iterate through tasks
445
+ searchable_tasks = tasks.map do |task|
446
+ { original: task, name: task.name }
447
+ end
448
+
449
+ # Only proceed if we have tasks to match against
450
+ return nil if searchable_tasks.empty?
451
+
452
+ matcher = FuzzyMatch.new(searchable_tasks, read: :name)
453
+ match = matcher.find(target_task.name, threshold: @task_match_threshold)
454
+ match[:original] if match
180
455
  end
181
456
  end
182
457
  end
data/lib/moco/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- VERSION = "0.1.2"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/moco.rb CHANGED
@@ -1,8 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+ require "active_support/inflector"
6
+
3
7
  require_relative "moco/version"
4
- require_relative "moco/entities"
5
- require_relative "moco/api"
8
+
9
+ # Core classes needed by entities
10
+ require_relative "moco/connection"
11
+ require_relative "moco/collection_proxy"
12
+ require_relative "moco/nested_collection_proxy"
13
+
14
+ # New API (v2)
15
+ require_relative "moco/entities/base_entity"
16
+ require_relative "moco/entities/project"
17
+ require_relative "moco/entities/activity"
18
+ require_relative "moco/entities/user"
19
+ require_relative "moco/entities/company"
20
+ require_relative "moco/entities/task"
21
+ require_relative "moco/entities/invoice"
22
+ require_relative "moco/entities/deal"
23
+ require_relative "moco/entities/expense"
24
+ require_relative "moco/entities/web_hook"
25
+ require_relative "moco/entities/schedule"
26
+ require_relative "moco/entities/presence"
27
+ require_relative "moco/entities/holiday"
28
+ require_relative "moco/entities/planning_entry"
29
+ require_relative "moco/client"
30
+ require_relative "moco/entity_collection"
31
+
6
32
  require_relative "moco/sync"
7
33
 
8
34
  module MOCO
data/moco.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/moco/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "moco-ruby"
7
+ spec.version = MOCO::VERSION
8
+ spec.authors = ["Teal Bauer"]
9
+ spec.email = ["rubygems@teal.is"]
10
+
11
+ spec.summary = "A Ruby Gem to interact with the MOCO (mocoapp.com) API."
12
+ spec.homepage = "https://github.com/starsong-consulting/moco-ruby"
13
+ spec.required_ruby_version = ">= 3.2.0"
14
+ spec.license = "Apache-2.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/starsong-consulting/moco-ruby"
18
+ spec.metadata["changelog_uri"] = "https://github.com/starsong-consulting/moco-ruby/blob/main/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "activesupport", "~> 7.0"
32
+ spec.add_dependency "faraday", "~> 2.9.0"
33
+ spec.add_dependency "fuzzy_match", "~> 2.1.0"
34
+
35
+ spec.metadata["rubygems_mfa_required"] = "true"
36
+ end