moco-ruby 0.1.1 → 1.0.0.alpha

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,7 +1,7 @@
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
@@ -9,9 +9,9 @@ module MOCO
9
9
  attr_reader :project_mapping, :task_mapping, :source_projects, :target_projects
10
10
  attr_accessor :project_match_threshold, :task_match_threshold, :dry_run
11
11
 
12
- def initialize(source_instance_api, target_instance_api, **args)
13
- @source_api = source_instance_api
14
- @target_api = target_instance_api
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, {})
@@ -24,12 +24,12 @@ module MOCO
24
24
  build_initial_mappings
25
25
  end
26
26
 
27
- # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
27
+ # rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
28
28
  def sync(&callbacks)
29
29
  results = []
30
30
 
31
- source_activities_r = @source_api.get_activities(@filters.fetch(:source, {}))
32
- target_activities_r = @target_api.get_activities(@filters.fetch(:target, {}))
31
+ source_activities_r = @source.activities.all(@filters.fetch(:source, {}))
32
+ target_activities_r = @target.activities.all(@filters.fetch(:target, {}))
33
33
 
34
34
  source_activities_grouped = source_activities_r.group_by(&:date).transform_values do |activities|
35
35
  activities.group_by(&:project)
@@ -38,6 +38,9 @@ module MOCO
38
38
  activities.group_by(&:project)
39
39
  end
40
40
 
41
+ used_source_activities = []
42
+ used_target_activities = []
43
+
41
44
  source_activities_grouped.each do |date, activities_by_project|
42
45
  activities_by_project.each do |project, source_activities|
43
46
  target_activities = target_activities_grouped.fetch(date, {}).fetch(@project_mapping[project.id], [])
@@ -46,9 +49,6 @@ module MOCO
46
49
  matches = calculate_matches(source_activities, target_activities)
47
50
  matches.sort_by! { |match| -match[:score] }
48
51
 
49
- used_source_activities = []
50
- used_target_activities = []
51
-
52
52
  matches.each do |match|
53
53
  source_activity, target_activity = match[:activity]
54
54
  score = match[:score]
@@ -70,14 +70,14 @@ module MOCO
70
70
  end
71
71
  callbacks&.call(:update, source_activity, best_match)
72
72
  unless @dry_run
73
- results << @target_api.update_activity(best_match)
73
+ results << @target.activities.update(best_match)
74
74
  callbacks&.call(:updated, source_activity, best_match, results.last)
75
75
  end
76
76
  when 0...60
77
77
  # <60 - no good match found, create new entry
78
78
  callbacks&.call(:create, source_activity, expected_target_activity)
79
79
  unless @dry_run
80
- results << @target_api.create_activity(expected_target_activity)
80
+ results << @target.activities.create(expected_target_activity)
81
81
  callbacks&.call(:created, source_activity, best_match, results.last)
82
82
  end
83
83
  end
@@ -87,6 +87,19 @@ module MOCO
87
87
  end
88
88
  end
89
89
  end
90
+
91
+ source_activities_r.each do |source_activity|
92
+ next if 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.activities.create(expected_target_activity)
99
+ callbacks&.call(:created, source_activity, expected_target_activity, results.last)
100
+ end
101
+ end
102
+
90
103
  results
91
104
  end
92
105
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -105,7 +118,7 @@ module MOCO
105
118
  source_activities.each do |source_activity|
106
119
  target_activities.each do |target_activity|
107
120
  score = score_activity_match(get_expected_target_activity(source_activity), target_activity)
108
- matches << { activity: [source_activity, target_activity], score: score }
121
+ matches << { activity: [source_activity, target_activity], score: }
109
122
  end
110
123
  end
111
124
  matches
@@ -138,8 +151,26 @@ module MOCO
138
151
  # rubocop:enable Metrics/AbcSize
139
152
 
140
153
  def fetch_assigned_projects
141
- @source_projects = @source_api.get_assigned_projects(**@filters.fetch(:source, {}).merge(active: "true"))
142
- @target_projects = @target_api.get_assigned_projects(**@filters.fetch(:target, {}).merge(active: "true"))
154
+ @source_projects = @source.projects.all(**@filters.fetch(:source, {}), active: "true")
155
+ @target_projects = @target.projects.all(**@filters.fetch(:target, {}), active: "true")
156
+
157
+ # Ensure we have proper collections
158
+ @source_projects = if @source_projects.is_a?(MOCO::EntityCollection)
159
+ @source_projects
160
+ else
161
+ MOCO::EntityCollection.new(@source,
162
+ "projects", "Project").tap do |c|
163
+ c.instance_variable_set(:@items, [@source_projects])
164
+ end
165
+ end
166
+ @target_projects = if @target_projects.is_a?(MOCO::EntityCollection)
167
+ @target_projects
168
+ else
169
+ MOCO::EntityCollection.new(@target,
170
+ "projects", "Project").tap do |c|
171
+ c.instance_variable_set(:@items, [@target_projects])
172
+ end
173
+ end
143
174
  end
144
175
 
145
176
  def build_initial_mappings
@@ -156,13 +187,37 @@ module MOCO
156
187
  end
157
188
 
158
189
  def match_project(target_project)
159
- matcher = FuzzyMatch.new(@source_projects, read: :name)
160
- matcher.find(target_project.name, threshold: @project_match_threshold)
190
+ # Create array of search objects manually since we can't call map on EntityCollection
191
+ searchable_projects = []
192
+
193
+ # Manually iterate since we can't rely on Enumerable methods
194
+ @source_projects.each do |project|
195
+ warn project.inspect
196
+ searchable_projects << { original: project, name: project.name }
197
+ end
198
+
199
+ matcher = FuzzyMatch.new(searchable_projects, read: :name)
200
+ match = matcher.find(target_project.name, threshold: @project_match_threshold)
201
+ match[:original] if match
161
202
  end
162
203
 
163
204
  def match_task(target_task, source_project)
164
- matcher = FuzzyMatch.new(source_project.tasks, read: :name)
165
- matcher.find(target_task.name, threshold: @task_match_threshold)
205
+ # Get tasks from the source project
206
+ tasks = source_project.tasks
207
+
208
+ # Create array of search objects manually since we can't rely on Enumerable methods
209
+
210
+ # Manually iterate through tasks
211
+ searchable_tasks = tasks.map do |task|
212
+ { original: task, name: task.name }
213
+ end
214
+
215
+ # Only proceed if we have tasks to match against
216
+ return nil if searchable_tasks.empty?
217
+
218
+ matcher = FuzzyMatch.new(searchable_tasks, read: :name)
219
+ match = matcher.find(target_task.name, threshold: @task_match_threshold)
220
+ match[:original] if match
166
221
  end
167
222
  end
168
223
  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.1"
4
+ VERSION = "1.0.0.alpha"
5
5
  end
data/lib/moco.rb CHANGED
@@ -1,8 +1,32 @@
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
+ # New API (v2)
10
+ require_relative "moco/entities/base_entity"
11
+ require_relative "moco/entities/project"
12
+ require_relative "moco/entities/activity"
13
+ require_relative "moco/entities/user"
14
+ require_relative "moco/entities/company"
15
+ require_relative "moco/entities/task"
16
+ require_relative "moco/entities/invoice"
17
+ require_relative "moco/entities/deal"
18
+ require_relative "moco/entities/expense"
19
+ require_relative "moco/entities/web_hook"
20
+ require_relative "moco/entities/schedule"
21
+ require_relative "moco/entities/presence"
22
+ require_relative "moco/entities/holiday"
23
+ require_relative "moco/entities/planning_entry"
24
+ require_relative "moco/client"
25
+ require_relative "moco/connection"
26
+ require_relative "moco/collection_proxy"
27
+ require_relative "moco/nested_collection_proxy"
28
+ require_relative "moco/entity_collection"
29
+
6
30
  require_relative "moco/sync"
7
31
 
8
32
  module MOCO
data/mocurl.rb CHANGED
@@ -58,42 +58,59 @@ end
58
58
 
59
59
  # Load default API key from config
60
60
  config = YAML.load_file("config.yml")
61
- options[:api_key] ||= config["instances"].fetch(subdomain, nil)&.fetch("api_key", nil)
61
+ options[:api_key] ||= config["instances"].dig(subdomain, "api_key")
62
62
 
63
63
  warn "Error: No API key found for `#{subdomain}' and none given, continuing without" if options[:api_key].nil?
64
64
 
65
- api = MOCO::API.new(subdomain, options[:api_key])
66
-
67
- case options[:method]
68
- when "GET"
69
- result = api.get(url)
70
- when "DELETE"
71
- result = api.delete(url)
72
- when "POST"
73
- result = api.post(url, options[:data])
74
- when "PUT"
75
- result = api.put(url, options[:data])
76
- when "PATCH"
77
- result = api.patch(url, options[:data])
78
- else
79
- puts "Error: Invalid HTTP Method: #{options[:method]}"
80
- exit 1
81
- end
65
+ client = MOCO::Client.new(subdomain: subdomain, api_key: options[:api_key])
66
+
67
+ # Extract path from URL
68
+ path = url.gsub(%r{https?://#{subdomain}\.mocoapp\.com/api/v1/}, "")
69
+
70
+ begin
71
+ # Make request using the client's connection directly
72
+ result = case options[:method]
73
+ when "GET"
74
+ client.connection.get(path)
75
+ when "DELETE"
76
+ client.connection.delete(path)
77
+ when "POST"
78
+ client.connection.post(path, options[:data])
79
+ when "PUT"
80
+ client.connection.put(path, options[:data])
81
+ when "PATCH"
82
+ client.connection.patch(path, options[:data])
83
+ else
84
+ puts "Error: Invalid HTTP Method: #{options[:method]}"
85
+ exit 1
86
+ end
87
+
88
+ if options[:verbose]
89
+ puts "> #{options[:method]} #{url}"
90
+ # Print request details if available
91
+ if result.env&.request_headers
92
+ puts(result.env.request_headers.map do |k, v|
93
+ "> #{k}: #{k == "Authorization" ? "#{v[0...16]}<REDACTED>#{v[-4..]}" : v}"
94
+ end)
95
+ puts ">"
96
+ puts result.env.request_body.split.map { |l| "> #{l}" }.join if result.env.request_body
97
+ puts "---"
98
+ puts "< #{result.status} #{result.reason_phrase}"
99
+ puts(result.headers.map { |k, v| "< #{k}: #{v}" })
100
+ else
101
+ puts "> Request details not available in this response format"
102
+ end
103
+ puts ""
104
+ end
82
105
 
83
- if options[:verbose]
84
- puts "> #{options[:method]} #{result.env.url}"
85
- puts(result.env.request_headers.map do |k, v|
86
- "> #{k}: #{k == "Authorization" ? "#{v[0...16]}<REDACTED>#{v[-4..]}" : v}"
87
- end)
88
- puts ">"
89
- puts result.env.request_body.split.map { |l| "> #{l}" }.join if result.env.request_body
90
- puts "---"
91
- puts "< #{result.status} #{result.reason_phrase}"
92
- puts(result.headers.map { |k, v| "< #{k}: #{v}" })
93
- puts ""
94
- end
95
- if options[:no_format]
96
- puts result.body.to_json
97
- else
98
- puts JSON.pretty_generate(result.body)
106
+ # Format the response
107
+ response_data = result.body
108
+ if options[:no_format]
109
+ puts response_data.to_json
110
+ else
111
+ puts JSON.pretty_generate(response_data)
112
+ end
113
+ rescue StandardError => e
114
+ puts "Error: #{e.message}"
115
+ exit 1
99
116
  end
data/sync_activity.rb CHANGED
@@ -64,12 +64,12 @@ config = YAML.load_file("config.yml")
64
64
  source_config = config["instances"].fetch(source_instance, nil)
65
65
  target_config = config["instances"].fetch(target_instance, nil)
66
66
 
67
- source_api = MOCO::API.new(source_instance, source_config["api_key"])
68
- target_api = MOCO::API.new(target_instance, target_config["api_key"])
67
+ source_client = MOCO::Client.new(subdomain: source_instance, api_key: source_config["api_key"])
68
+ target_client = MOCO::Client.new(subdomain: target_instance, api_key: target_config["api_key"])
69
69
 
70
70
  syncer = MOCO::Sync.new(
71
- source_api,
72
- target_api,
71
+ source_client,
72
+ target_client,
73
73
  project_match_threshold: options[:match_project_threshold],
74
74
  task_match_threshold: options[:match_task_threshold],
75
75
  filters: {
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moco-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Teal Bauer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-27 00:00:00.000000000 Z
11
+ date: 2025-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: faraday
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -55,20 +69,40 @@ files:
55
69
  - README.md
56
70
  - Rakefile
57
71
  - config.yml.sample
72
+ - examples/v2_api_example.rb
58
73
  - lib/moco.rb
59
- - lib/moco/api.rb
74
+ - lib/moco/client.rb
75
+ - lib/moco/collection_proxy.rb
76
+ - lib/moco/connection.rb
60
77
  - lib/moco/entities.rb
78
+ - lib/moco/entities/activity.rb
79
+ - lib/moco/entities/base_entity.rb
80
+ - lib/moco/entities/company.rb
81
+ - lib/moco/entities/deal.rb
82
+ - lib/moco/entities/expense.rb
83
+ - lib/moco/entities/holiday.rb
84
+ - lib/moco/entities/invoice.rb
85
+ - lib/moco/entities/planning_entry.rb
86
+ - lib/moco/entities/presence.rb
87
+ - lib/moco/entities/project.rb
88
+ - lib/moco/entities/schedule.rb
89
+ - lib/moco/entities/task.rb
90
+ - lib/moco/entities/user.rb
91
+ - lib/moco/entities/web_hook.rb
92
+ - lib/moco/entity_collection.rb
93
+ - lib/moco/helpers.rb
94
+ - lib/moco/nested_collection_proxy.rb
61
95
  - lib/moco/sync.rb
62
96
  - lib/moco/version.rb
63
97
  - mocurl.rb
64
98
  - sync_activity.rb
65
- homepage: https://github.com/moeffju/moco-ruby
99
+ homepage: https://github.com/starsong-consulting/moco-ruby
66
100
  licenses:
67
101
  - Apache-2.0
68
102
  metadata:
69
- homepage_uri: https://github.com/moeffju/moco-ruby
70
- source_code_uri: https://github.com/moeffju/moco-ruby
71
- changelog_uri: https://github.com/moeffju/moco-ruby/blob/main/CHANGELOG.md
103
+ homepage_uri: https://github.com/starsong-consulting/moco-ruby
104
+ source_code_uri: https://github.com/starsong-consulting/moco-ruby
105
+ changelog_uri: https://github.com/starsong-consulting/moco-ruby/blob/main/CHANGELOG.md
72
106
  rubygems_mfa_required: 'true'
73
107
  post_install_message:
74
108
  rdoc_options: []
@@ -78,12 +112,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
112
  requirements:
79
113
  - - ">="
80
114
  - !ruby/object:Gem::Version
81
- version: 2.6.0
115
+ version: 3.2.0
82
116
  required_rubygems_version: !ruby/object:Gem::Requirement
83
117
  requirements:
84
- - - ">="
118
+ - - ">"
85
119
  - !ruby/object:Gem::Version
86
- version: '0'
120
+ version: 1.3.1
87
121
  requirements: []
88
122
  rubygems_version: 3.4.1
89
123
  signing_key:
data/lib/moco/api.rb DELETED
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "faraday"
4
- require_relative "entities"
5
-
6
- module MOCO
7
- # MOCO::API abstracts access to the MOCO API and its entities
8
- class API
9
- def initialize(subdomain, api_key)
10
- @subdomain = subdomain
11
- @api_key = api_key
12
- @conn = Faraday.new do |f|
13
- f.request :json
14
- f.response :json
15
- f.request :authorization, "Token", "token=#{@api_key}" if @api_key
16
- f.url_prefix = "https://#{@subdomain}.mocoapp.com/api/v1"
17
- end
18
- end
19
-
20
- %w[get post put patch delete].each do |method|
21
- define_method(method) do |path, *args|
22
- @conn.send(method, path, *args)
23
- end
24
- end
25
-
26
- def get_projects(**args)
27
- response = @conn.get("projects?#{Faraday::Utils.build_query(args)}")
28
- parse_projects_response(response.body)
29
- end
30
-
31
- def get_assigned_projects(**args)
32
- response = @conn.get("projects/assigned?#{Faraday::Utils.build_query(args)}")
33
- parse_projects_response(response.body)
34
- end
35
-
36
- def get_activities(filters = {})
37
- response = @conn.get("activities?#{Faraday::Utils.build_query(filters)}")
38
- parse_activities_response(response.body)
39
- end
40
-
41
- def create_activity(activity)
42
- api_entity = activity.to_h.except(:id, :project, :user, :customer).tap do |h|
43
- h[:project_id] = activity.project.id
44
- h[:task_id] = activity.task.id
45
- end
46
- @conn.post("activities", api_entity)
47
- end
48
-
49
- def update_activity(activity)
50
- api_entity = activity.to_h.except(:project, :user, :customer).tap do |h|
51
- h[:project_id] = activity.project.id
52
- h[:task_id] = activity.task.id
53
- end
54
- @conn.put("activities/#{activity.id}", api_entity)
55
- end
56
-
57
- private
58
-
59
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
60
- def parse_projects_response(data)
61
- data.map do |project_data|
62
- Project.new.tap do |project|
63
- project.id = project_data["id"]
64
- project.name = project_data["name"]
65
- project.customer = parse_customer_reference(project_data["customer"])
66
- project.tasks = project_data["tasks"].map do |task_data|
67
- Task.new.tap do |task|
68
- task.id = task_data["id"]
69
- task.name = task_data["name"]
70
- task.project_id = task_data["project_id"]
71
- task.billable = task_data["billable"]
72
- end
73
- end
74
- end
75
- end
76
- end
77
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
78
-
79
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
80
- def parse_activities_response(data)
81
- data.map do |activity_data|
82
- Activity.new.tap do |activity|
83
- activity.id = activity_data["id"]
84
- activity.date = activity_data["date"]
85
- activity.description = activity_data["description"]
86
- activity.user = parse_user_reference(activity_data["user"])
87
- activity.customer = parse_customer_reference(activity_data["customer"])
88
- activity.project = parse_project_reference(activity_data["project"])
89
- activity.task = parse_task_reference(activity_data["task"])
90
- activity.hours = activity_data["hours"]
91
- activity.seconds = activity_data["seconds"]
92
- activity.billable = activity_data["billable"]
93
- activity.billed = activity_data["billed"]
94
- activity.tag = activity_data["tag"]
95
- end
96
- end
97
- end
98
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
99
-
100
- def parse_project_reference(project_data)
101
- Project.new.tap do |project|
102
- project.id = project_data["id"]
103
- project.name = project_data["name"]
104
- end
105
- end
106
-
107
- def parse_task_reference(task_data)
108
- Task.new.tap do |task|
109
- task.id = task_data["id"]
110
- task.name = task_data["name"]
111
- end
112
- end
113
-
114
- def parse_user_reference(user_data)
115
- User.new.tap do |user|
116
- user.id = user_data["id"]
117
- user.firstname = user_data["firstname"]
118
- user.lastname = user_data["lastname"]
119
- end
120
- end
121
-
122
- def parse_customer_reference(customer_data)
123
- Customer.new.tap do |customer|
124
- customer.id = customer_data["id"]
125
- customer.name = customer_data["name"]
126
- end
127
- end
128
- end
129
- end