youcanbookme 0.0.1.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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gem-push.yml +37 -0
  3. data/.github/workflows/test.yml +21 -0
  4. data/.gitignore +10 -0
  5. data/.rubocom.yml +69 -0
  6. data/CHANGELOG.md +7 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +119 -0
  11. data/Rakefile +12 -0
  12. data/bin/console +7 -0
  13. data/bin/setup +6 -0
  14. data/lib/youcanbookme.rb +22 -0
  15. data/lib/youcanbookme/client.rb +85 -0
  16. data/lib/youcanbookme/common_module.rb +32 -0
  17. data/lib/youcanbookme/configuration.rb +20 -0
  18. data/lib/youcanbookme/error.rb +15 -0
  19. data/lib/youcanbookme/http_command.rb +94 -0
  20. data/lib/youcanbookme/models/account.rb +125 -0
  21. data/lib/youcanbookme/models/action.rb +91 -0
  22. data/lib/youcanbookme/models/answer.rb +19 -0
  23. data/lib/youcanbookme/models/appointment_type.rb +7 -0
  24. data/lib/youcanbookme/models/booking.rb +167 -0
  25. data/lib/youcanbookme/models/booking_lobby.rb +13 -0
  26. data/lib/youcanbookme/models/calendar.rb +67 -0
  27. data/lib/youcanbookme/models/caligraph_calendar.rb +29 -0
  28. data/lib/youcanbookme/models/caligraph_event.rb +8 -0
  29. data/lib/youcanbookme/models/caligraph_link.rb +17 -0
  30. data/lib/youcanbookme/models/caligraph_local_account.rb +47 -0
  31. data/lib/youcanbookme/models/caligraph_permission.rb +32 -0
  32. data/lib/youcanbookme/models/caligraph_remote_account.rb +23 -0
  33. data/lib/youcanbookme/models/caligraph_safe_local_account.rb +13 -0
  34. data/lib/youcanbookme/models/card.rb +25 -0
  35. data/lib/youcanbookme/models/event.rb +98 -0
  36. data/lib/youcanbookme/models/instant.rb +9 -0
  37. data/lib/youcanbookme/models/link.rb +17 -0
  38. data/lib/youcanbookme/models/local_account.rb +180 -0
  39. data/lib/youcanbookme/models/model_utils.rb +88 -0
  40. data/lib/youcanbookme/models/name_and_address.rb +25 -0
  41. data/lib/youcanbookme/models/participant.rb +25 -0
  42. data/lib/youcanbookme/models/permission.rb +39 -0
  43. data/lib/youcanbookme/models/profile.rb +114 -0
  44. data/lib/youcanbookme/models/profile_afterwards.rb +34 -0
  45. data/lib/youcanbookme/models/profile_appointment_type.rb +23 -0
  46. data/lib/youcanbookme/models/profile_appointment_types.rb +27 -0
  47. data/lib/youcanbookme/models/profile_calendar.rb +21 -0
  48. data/lib/youcanbookme/models/profile_calendars.rb +19 -0
  49. data/lib/youcanbookme/models/profile_cancel_or_reschedule.rb +21 -0
  50. data/lib/youcanbookme/models/profile_display.rb +55 -0
  51. data/lib/youcanbookme/models/profile_local_account.rb +19 -0
  52. data/lib/youcanbookme/models/profile_payments.rb +23 -0
  53. data/lib/youcanbookme/models/profile_remote_account.rb +25 -0
  54. data/lib/youcanbookme/models/profile_remote_reminder.rb +15 -0
  55. data/lib/youcanbookme/models/profile_team_member.rb +33 -0
  56. data/lib/youcanbookme/models/profile_team_members.rb +29 -0
  57. data/lib/youcanbookme/models/profile_tentative.rb +16 -0
  58. data/lib/youcanbookme/models/profile_times.rb +79 -0
  59. data/lib/youcanbookme/models/profile_vouchers.rb +17 -0
  60. data/lib/youcanbookme/models/provider.rb +17 -0
  61. data/lib/youcanbookme/models/purchase.rb +126 -0
  62. data/lib/youcanbookme/models/purchases_sync_changes.rb +19 -0
  63. data/lib/youcanbookme/models/query.rb +8 -0
  64. data/lib/youcanbookme/models/question.rb +24 -0
  65. data/lib/youcanbookme/models/reminder.rb +21 -0
  66. data/lib/youcanbookme/models/remote_account.rb +63 -0
  67. data/lib/youcanbookme/models/team_member.rb +8 -0
  68. data/lib/youcanbookme/models/template_event.rb +17 -0
  69. data/lib/youcanbookme/models/transaction.rb +42 -0
  70. data/lib/youcanbookme/models/warning.rb +27 -0
  71. data/lib/youcanbookme/version.rb +5 -0
  72. data/youcanbookme.gemspec +41 -0
  73. metadata +244 -0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ # YouCanBook.me utility module.
5
+ module CommonModule
6
+ def error_log(msg)
7
+ log msg, :error
8
+ end
9
+
10
+ def warn_log(msg)
11
+ log msg, :warn
12
+ end
13
+
14
+ def info_log(msg)
15
+ log msg, :info
16
+ end
17
+
18
+ def debug_log(msg)
19
+ log msg, :debug
20
+ end
21
+
22
+ private
23
+
24
+ def log(msg, level = :info)
25
+ logger = YouCanBookMe.configuration.logger
26
+ return unless logger
27
+ return unless logger.respond_to? level
28
+
29
+ logger.send level, msg
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module YouCanBookMe
6
+ # YouCanBook.me APIs client configuration.
7
+ class Configuration
8
+ # @return [String]
9
+ attr_accessor :account_id
10
+ # @return [String]
11
+ attr_accessor :password_or_token
12
+ # @return [Logger]
13
+ attr_accessor :logger
14
+
15
+ def initialize
16
+ @logger = Logger.new STDOUT
17
+ @logger.level = :warn
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ # YouCanBookMe module's base error object
5
+ class Error < StandardError
6
+ include CommonModule
7
+
8
+ def initialize(message = nil)
9
+ msg = "#{self.class} occured."
10
+ msg += " message:#{message}"
11
+ warn_log msg
12
+ super message
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+
6
+ module YouCanBookMe
7
+ # Command for HTTP request.
8
+ class HttpCommand
9
+ include CommonModule
10
+
11
+ def initialize(account_id, password_or_token, host = nil)
12
+ @account_id = account_id
13
+ @password_or_token = password_or_token
14
+ @host = host || YouCanBookMe::Client::API_HOST
15
+ end
16
+
17
+ #
18
+ # send GET http request.
19
+ #
20
+ # @param [String] path String or URI to access.
21
+ # @param [Hash] params Hash of URI query unencoded key/value pairs.
22
+ def get(path, params = {})
23
+ debug_log "GET #{connection.build_url("#{@host}#{path}", params)}"
24
+ res = connection.get path, params
25
+ debug_log "Response status:#{res.status}, body:#{res.body}"
26
+ res
27
+ end
28
+
29
+ #
30
+ # send POST http request.
31
+ #
32
+ # @param [String] path String or URI to access.
33
+ # @param [Hash] body_params
34
+ # The request body that will eventually be converted to JSON.
35
+ def post(path, body_params = {})
36
+ debug_log "POST #{@host}#{path} body:#{body_params}"
37
+ headers = { 'Content-Type' => 'application/json' }
38
+ res = connection.run_request :post, path, body_params.to_json, headers
39
+ debug_log "Response status:#{res.status}, body:#{res.body}"
40
+ res
41
+ end
42
+
43
+ #
44
+ # send PUT http request.
45
+ #
46
+ # @param [String] path String or URI to access.
47
+ # @param [Hash] body_params
48
+ # The request body that will eventually be converted to JSON.
49
+ def put(path, body_params = {})
50
+ debug_log "PUT #{@host}#{path} body:#{body_params}"
51
+ headers = { 'Content-Type' => 'application/json' }
52
+ res = connection.run_request :put, path, body_params.to_json, headers
53
+ debug_log "Response status:#{res.status}, body:#{res.body}"
54
+ res
55
+ end
56
+
57
+ #
58
+ # send PATCH http request.
59
+ #
60
+ # @param [String] path String or URI to access.
61
+ # @param [Hash] body_params
62
+ # The request body that will eventually be converted to JSON.
63
+ def patch(path, body_params = {})
64
+ debug_log "PATCH #{@host}#{path} body:#{body_params}"
65
+ headers = { 'Content-Type' => 'application/json' }
66
+ res = connection.run_request :put, path, body_params.to_json, headers
67
+ debug_log "Response status:#{res.status}, body:#{res.body}"
68
+ res
69
+ end
70
+
71
+ #
72
+ # send DELETE http request.
73
+ #
74
+ # @param [String] path String or URI to access.
75
+ # @param [Hash] params Hash of URI query unencoded key/value pairs.
76
+ def delete(path, params = {})
77
+ debug_log "DELETE #{@host}#{path} params:#{params}"
78
+ res = connection.delete path, params
79
+ debug_log "Response status:#{res.status}, body:#{res.body}"
80
+ res
81
+ end
82
+
83
+ private
84
+
85
+ def connection
86
+ Faraday.new(
87
+ url: @host
88
+ ) do |builder|
89
+ builder.use Faraday::Request::BasicAuthentication, @account_id, @password_or_token
90
+ builder.response :json, parser_options: { symbolize_names: true }, content_type: /\bjson$/
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ # YouCanBookMe's Account model.
5
+ class Account
6
+ include ModelUtils
7
+
8
+ ASSOCIATION = {
9
+ children: Account,
10
+ localAccount: CaligraphLocalAccount,
11
+ parent: Account,
12
+ profiles: Profile,
13
+ warnings: Warning
14
+ }.freeze
15
+
16
+ # @return [String]
17
+ attr_accessor :caligraphKey
18
+ # @return [Array<YouCanBookMe::Account>]
19
+ attr_accessor :children
20
+ # @return [String]
21
+ attr_accessor :clientState
22
+ # @return [Boolean]
23
+ attr_accessor :contactHigh
24
+ # @return [Boolean]
25
+ attr_accessor :contactImportant
26
+ # @return [Boolean]
27
+ attr_accessor :contactLow
28
+ # @return [String]
29
+ attr_accessor :contactMobile
30
+ # @return [String]
31
+ attr_accessor :createdAt
32
+ # @return [Integer]
33
+ attr_accessor :deleteBookingsAfterDays
34
+ # @return [Integer]
35
+ attr_accessor :deletedBookingsCount
36
+ # @return [String]
37
+ attr_accessor :email
38
+ # @return [String]
39
+ attr_accessor :familyName
40
+ # @return [Integer]
41
+ attr_accessor :feeCredit
42
+ # @return [String]
43
+ attr_accessor :givenName
44
+ # @return [String]
45
+ attr_accessor :gmailAccessToken
46
+ # @return [String]
47
+ attr_accessor :gmailAccessTokenExpires
48
+ # @return [String]
49
+ attr_accessor :gmailAddress
50
+ # @return [String]
51
+ attr_accessor :gmailRefreshToken
52
+ # @return [Boolean]
53
+ attr_accessor :googleAppsAccount
54
+ # @return [Boolean]
55
+ attr_accessor :gridBranding
56
+ # @return [Boolean]
57
+ attr_accessor :hasBookings
58
+ # @return [String]
59
+ attr_accessor :id
60
+ # @return [YouCanBookMe::CaligraphLocalAccount]
61
+ attr_accessor :localAccount
62
+ # @return [String]
63
+ attr_accessor :locale
64
+ # @return [Account]
65
+ attr_accessor :loggedIn
66
+ # @return [String]
67
+ attr_accessor :organisation
68
+ # @return [YouCanBookMe::Account]
69
+ attr_accessor :parent
70
+ # @return [String]
71
+ attr_accessor :parentId
72
+ # @return [Boolean]
73
+ attr_accessor :privilegedLogin
74
+ # @return [Array<YouCanBookMe::Profile>]
75
+ attr_accessor :profiles
76
+ # @return [String]
77
+ attr_accessor :realtimeTopic
78
+ # @return [String]
79
+ attr_accessor :requestedAction
80
+ # @return [String]
81
+ attr_accessor :salesforceAccessToken
82
+ # @return [String]
83
+ attr_accessor :salesforceEmailAddress
84
+ # @return [String]
85
+ attr_accessor :salesforceInstanceUrl
86
+ # @return [String]
87
+ attr_accessor :salesforceRefreshToken
88
+ # @return [String]
89
+ attr_accessor :sector
90
+ # @return [Integer]
91
+ attr_accessor :smsCredits
92
+ # @return [String]
93
+ attr_accessor :stripeAccessToken
94
+ # @return [String]
95
+ attr_accessor :stripePublishableKey
96
+ # @return [String]
97
+ attr_accessor :stripeRefreshToken
98
+ # @return [String]
99
+ attr_accessor :stripeUserId
100
+ # @return [String]
101
+ attr_accessor :tags
102
+ # @return [Boolean]
103
+ attr_accessor :terms
104
+ # @return [String]
105
+ attr_accessor :termsDate
106
+ # @return [String]
107
+ attr_accessor :timeZone
108
+ # @return [String]
109
+ attr_accessor :uploadCode
110
+ # @return [Array<YouCanBookMe::Warning>]
111
+ attr_accessor :warnings
112
+ # @return [Boolean]
113
+ attr_accessor :welcomed
114
+ # @return [String]
115
+ attr_accessor :zapierKey
116
+ # @return [String]
117
+ attr_accessor :zoomAccessToken
118
+ # @return [String]
119
+ attr_accessor :zoomAccessTokenExpires
120
+ # @return [String]
121
+ attr_accessor :zoomEmailAddress
122
+ # @return [String]
123
+ attr_accessor :zoomRefreshToken
124
+ end
125
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ # YouCanBookMe's Action model.
5
+ class Action
6
+ include ModelUtils
7
+
8
+ # @return [String]
9
+ attr_accessor :accountId
10
+ # @return [String]
11
+ attr_accessor :anchor
12
+ # @return [Boolean]
13
+ attr_accessor :attachIcs
14
+ # @return [String]
15
+ attr_accessor :body
16
+ # @return [String]
17
+ attr_accessor :bookingId
18
+ # @return [String]
19
+ attr_accessor :cc
20
+ # @return [String]
21
+ attr_accessor :created
22
+ # @return [Integer]
23
+ attr_accessor :creditsUsed
24
+ # @return [String]
25
+ attr_accessor :defaultAsString
26
+ # @return [Boolean]
27
+ attr_accessor :deletable
28
+ # @return [String]
29
+ attr_accessor :displayFiredAtMediumMedium
30
+ # @return [String]
31
+ attr_accessor :displayFiredAtMediumShort
32
+ # @return [String]
33
+ attr_accessor :displayFiredAtShortShort
34
+ # @return [String]
35
+ attr_accessor :displayLocale
36
+ # @return [String]
37
+ attr_accessor :displayTimeZone
38
+ # @return [String]
39
+ attr_accessor :displayTypeLong
40
+ # @return [String]
41
+ attr_accessor :displayTypeShort
42
+ # @return [Boolean]
43
+ attr_accessor :editable
44
+ # @return [String]
45
+ attr_accessor :emailId
46
+ # @return [String]
47
+ attr_accessor :fireAt
48
+ # @return [Boolean]
49
+ attr_accessor :fireAtForceWeekday
50
+ # @return [String]
51
+ attr_accessor :firedAt
52
+ # @return [String]
53
+ attr_accessor :fromAddress
54
+ # @return [String]
55
+ attr_accessor :fromName
56
+ # @return [String]
57
+ attr_accessor :id
58
+ # @return [String]
59
+ attr_accessor :imageUrl
60
+ # @return [String]
61
+ attr_accessor :locale
62
+ # @return [Integer]
63
+ attr_accessor :offsetMinutes
64
+ # @return [String]
65
+ attr_accessor :parentId
66
+ # @return [String]
67
+ attr_accessor :profileId
68
+ # @return [String]
69
+ attr_accessor :status
70
+ # @return [String]
71
+ attr_accessor :stylingTemplate
72
+ # @return [String]
73
+ attr_accessor :subject
74
+ # @return [String]
75
+ attr_accessor :timeZone
76
+ # @return [String]
77
+ attr_accessor :timeline
78
+ # @return [String]
79
+ attr_accessor :title
80
+ # @return [String]
81
+ attr_accessor :to
82
+ # @return [String]
83
+ attr_accessor :type
84
+ # @return [String]
85
+ attr_accessor :updated
86
+ # @return [Boolean]
87
+ attr_accessor :withinQuota
88
+ # @return [Boolean]
89
+ attr_accessor :ycbmBranded
90
+ end
91
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ # YouCanBookMe's Answer model.
5
+ class Answer
6
+ include ModelUtils
7
+
8
+ ASSOCIATION = {
9
+ question: Question
10
+ }.freeze
11
+
12
+ # @return [String]
13
+ attr_accessor :code
14
+ # @return [YouCanBookMe::Question]
15
+ attr_accessor :question
16
+ # @return [String]
17
+ attr_accessor :string
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ class AppointmentType
5
+ include ModelUtils
6
+ end
7
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouCanBookMe
4
+ # YouCanBookMe's Booking model.
5
+ class Booking
6
+ include ModelUtils
7
+
8
+ ASSOCIATION = {
9
+ actions: Action,
10
+ answers: Answer,
11
+ appointmentTypes: ProfileAppointmentType,
12
+ caligraphEvent: CaligraphEvent,
13
+ databaseVersion: Booking,
14
+ lobby: BookingLobby,
15
+ profile: Profile,
16
+ services: ProfileAppointmentType,
17
+ teamMember: ProfileTeamMember
18
+ }.freeze
19
+
20
+ # @return [String]
21
+ attr_accessor :acceptedAt
22
+ # @return [String]
23
+ attr_accessor :accountId
24
+ # @return [Array<YouCanBookMe::Action>]
25
+ attr_accessor :actions
26
+ # @return [Array<YouCanBookMe::Answer>]
27
+ attr_accessor :answers
28
+ # @return [Array<String>]
29
+ attr_accessor :appointmentTypeIds
30
+ # @return [Array<YouCanBookMe::ProfileAppointmentType>]
31
+ attr_accessor :appointmentTypes
32
+ # @return [Array<String>]
33
+ attr_accessor :appointmentTypesIds
34
+ # @return [String]
35
+ attr_accessor :bookerSecret
36
+ # @return [YouCanBookMe::CaligraphEvent]
37
+ attr_accessor :caligraphEvent
38
+ # @return [Boolean]
39
+ attr_accessor :cancelReasonRequired
40
+ # @return [Boolean]
41
+ attr_accessor :cancellable
42
+ # @return [String]
43
+ attr_accessor :cancellationCode
44
+ # @return [String]
45
+ attr_accessor :cancellationReason
46
+ # @return [Boolean]
47
+ attr_accessor :cancelled
48
+ # @return [String]
49
+ attr_accessor :cancelledAt
50
+ # @return [String]
51
+ attr_accessor :cancelledBy
52
+ # @return [String]
53
+ attr_accessor :createdAt
54
+ # @return [String]
55
+ attr_accessor :currency
56
+ # @return [Integer]
57
+ attr_accessor :currencyFactor
58
+ # @return [YouCanBookMe::Booking]
59
+ attr_accessor :databaseVersion
60
+ # @return [String]
61
+ attr_accessor :displayBookerEndsAtMediumShort
62
+ # @return [String]
63
+ attr_accessor :displayBookerStartsAtMediumShort
64
+ # @return [String]
65
+ attr_accessor :displayCreatedAtMedium
66
+ # @return [String]
67
+ attr_accessor :displayCreatedAtShortShort
68
+ # @return [String]
69
+ attr_accessor :displayDurationFull
70
+ # @return [String]
71
+ attr_accessor :displayDurationShort
72
+ # @return [String]
73
+ attr_accessor :displayEndsAtMediumShort
74
+ # @return [String]
75
+ attr_accessor :displayLocale
76
+ # @return [String]
77
+ attr_accessor :displayPrice
78
+ # @return [String]
79
+ attr_accessor :displayStartsAtFullShortAndDurationFull
80
+ # @return [String]
81
+ attr_accessor :displayStartsAtMediumShort
82
+ # @return [String]
83
+ attr_accessor :displayTimeZone
84
+ # @return [Integer]
85
+ attr_accessor :durationMinutes
86
+ # @return [String]
87
+ attr_accessor :endsAt
88
+ # @return [String]
89
+ attr_accessor :endsAtUTC
90
+ # @return [Integer]
91
+ attr_accessor :expectedPrice
92
+ # @return [String]
93
+ attr_accessor :id
94
+ # @return [String]
95
+ attr_accessor :initiatedAt
96
+ # @return [String]
97
+ attr_accessor :ip
98
+ # @return [String]
99
+ attr_accessor :linkFields
100
+ # @return [YouCanBookMe::BookingLobby]
101
+ attr_accessor :lobby
102
+ # @return [String]
103
+ attr_accessor :lobbyPassword
104
+ # @return [String]
105
+ attr_accessor :locale
106
+ # @return [Integer]
107
+ attr_accessor :numberOfSlots
108
+ # @return [String]
109
+ attr_accessor :ownerSecret
110
+ # @return [String]
111
+ attr_accessor :paymentId
112
+ # @return [Boolean]
113
+ attr_accessor :preview
114
+ # @return [Integer]
115
+ attr_accessor :price
116
+ # @return [YouCanBookMe::Profile]
117
+ attr_accessor :profile
118
+ # @return [String]
119
+ attr_accessor :profileAccessCode
120
+ # @return [String]
121
+ attr_accessor :profileId
122
+ # @return [String]
123
+ attr_accessor :profilePassword
124
+ # @return [String]
125
+ attr_accessor :recaptchaResponse
126
+ # @return [String]
127
+ attr_accessor :ref
128
+ # @return [String]
129
+ attr_accessor :rejectedAt
130
+ # @return [String]
131
+ attr_accessor :remoteId
132
+ # @return [String]
133
+ attr_accessor :rescheduledAt
134
+ # @return [String]
135
+ attr_accessor :rescheduledBy
136
+ # @return [String]
137
+ attr_accessor :reviewAt
138
+ # @return [Array<String>]
139
+ attr_accessor :serviceIds
140
+ # @return [Array<YouCanBookMe::ProfileAppointmentType>]
141
+ attr_accessor :services
142
+ # @return [String]
143
+ attr_accessor :startRescheduledFrom
144
+ # @return [Instant]
145
+ attr_accessor :startRescheduledFromUTC
146
+ # @return [String]
147
+ attr_accessor :startsAt
148
+ # @return [String]
149
+ attr_accessor :startsAtUTC
150
+ # @return [String]
151
+ attr_accessor :stripeToken
152
+ # @return [YouCanBookMe::ProfileTeamMember]
153
+ attr_accessor :teamMember
154
+ # @return [String]
155
+ attr_accessor :teamMemberId
156
+ # @return [String]
157
+ attr_accessor :tentative
158
+ # @return [String]
159
+ attr_accessor :timeZone
160
+ # @return [String]
161
+ attr_accessor :timeline
162
+ # @return [String]
163
+ attr_accessor :title
164
+ # @return [Integer]
165
+ attr_accessor :units
166
+ end
167
+ end