forest_admin_datasource_pylon 1.41.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/README.md +179 -0
  4. data/Rakefile +6 -0
  5. data/forest_admin_datasource_pylon.gemspec +36 -0
  6. data/lib/forest_admin_datasource_pylon/client/writes.rb +88 -0
  7. data/lib/forest_admin_datasource_pylon/client.rb +436 -0
  8. data/lib/forest_admin_datasource_pylon/collections/account/api_filters.rb +43 -0
  9. data/lib/forest_admin_datasource_pylon/collections/account/schema_definition.rb +91 -0
  10. data/lib/forest_admin_datasource_pylon/collections/account/serializer.rb +21 -0
  11. data/lib/forest_admin_datasource_pylon/collections/account.rb +47 -0
  12. data/lib/forest_admin_datasource_pylon/collections/base_collection.rb +563 -0
  13. data/lib/forest_admin_datasource_pylon/collections/contact/api_filters.rb +38 -0
  14. data/lib/forest_admin_datasource_pylon/collections/contact/schema_definition.rb +93 -0
  15. data/lib/forest_admin_datasource_pylon/collections/contact/serializer.rb +20 -0
  16. data/lib/forest_admin_datasource_pylon/collections/contact.rb +45 -0
  17. data/lib/forest_admin_datasource_pylon/collections/cursor_collection.rb +131 -0
  18. data/lib/forest_admin_datasource_pylon/collections/fetch_all_collection.rb +192 -0
  19. data/lib/forest_admin_datasource_pylon/collections/issue/api_filters.rb +41 -0
  20. data/lib/forest_admin_datasource_pylon/collections/issue/id_lookup_reader.rb +88 -0
  21. data/lib/forest_admin_datasource_pylon/collections/issue/messages_embedder.rb +89 -0
  22. data/lib/forest_admin_datasource_pylon/collections/issue/schema_definition.rb +122 -0
  23. data/lib/forest_admin_datasource_pylon/collections/issue/serializer.rb +26 -0
  24. data/lib/forest_admin_datasource_pylon/collections/issue.rb +128 -0
  25. data/lib/forest_admin_datasource_pylon/collections/record_serialization.rb +84 -0
  26. data/lib/forest_admin_datasource_pylon/collections/relation_embedder.rb +101 -0
  27. data/lib/forest_admin_datasource_pylon/collections/team.rb +49 -0
  28. data/lib/forest_admin_datasource_pylon/collections/user.rb +69 -0
  29. data/lib/forest_admin_datasource_pylon/collections/writes.rb +417 -0
  30. data/lib/forest_admin_datasource_pylon/configuration.rb +84 -0
  31. data/lib/forest_admin_datasource_pylon/datasource.rb +53 -0
  32. data/lib/forest_admin_datasource_pylon/issue_enums.rb +20 -0
  33. data/lib/forest_admin_datasource_pylon/pagination/cursor_walker.rb +103 -0
  34. data/lib/forest_admin_datasource_pylon/plugins/close_issue/messages.rb +62 -0
  35. data/lib/forest_admin_datasource_pylon/plugins/close_issue.rb +141 -0
  36. data/lib/forest_admin_datasource_pylon/plugins/create_issue_with_notification/form_builder.rb +173 -0
  37. data/lib/forest_admin_datasource_pylon/plugins/create_issue_with_notification/payload.rb +72 -0
  38. data/lib/forest_admin_datasource_pylon/plugins/create_issue_with_notification.rb +175 -0
  39. data/lib/forest_admin_datasource_pylon/plugins/issue_targets.rb +51 -0
  40. data/lib/forest_admin_datasource_pylon/query/condition_tree_translator.rb +151 -0
  41. data/lib/forest_admin_datasource_pylon/query/filter_value.rb +135 -0
  42. data/lib/forest_admin_datasource_pylon/query/operator_maps.rb +108 -0
  43. data/lib/forest_admin_datasource_pylon/rate_limiter.rb +139 -0
  44. data/lib/forest_admin_datasource_pylon/rate_limits.rb +96 -0
  45. data/lib/forest_admin_datasource_pylon/retry_policy.rb +86 -0
  46. data/lib/forest_admin_datasource_pylon/schema/custom_fields_introspector.rb +203 -0
  47. data/lib/forest_admin_datasource_pylon/throttle.rb +21 -0
  48. data/lib/forest_admin_datasource_pylon/version.rb +3 -0
  49. data/lib/forest_admin_datasource_pylon.rb +70 -0
  50. metadata +152 -0
@@ -0,0 +1,86 @@
1
+ module ForestAdminDatasourcePylon
2
+ # Everything governing how the client reacts to a failed request, in one
3
+ # place: which statuses and exceptions are worth another attempt, on which
4
+ # verbs, and how long to wait.
5
+ class RetryPolicy
6
+ # What one attempt may be held back before the retry is abandoned instead.
7
+ # faraday-retry gives up outright when Retry-After exceeds max_interval
8
+ # (`return if retry_after > max_interval`), so this is also the bound on what
9
+ # a 429 costs: past it the client raises at once rather than sleeping.
10
+ #
11
+ # A fifth of a Pylon window rather than the whole of it, on purpose. Pylon
12
+ # quotas are per-minute, so a 429 carries a Retry-After of up to 60s, and
13
+ # honouring it three times over parked the calling thread for three minutes
14
+ # -- long after the Forest server timed out the request nobody will now read,
15
+ # and multiplied by the 20 sequential calls a single list view may spend
16
+ # (`MAX_ID_LOOKUPS`). The figure is what keeps the sleeps of one whole
17
+ # request under a minute: `max_retries` waits of this, plus the
18
+ # `RateLimiter::DEFAULT_MAX_WAIT` each of the four attempts may also pay.
19
+ #
20
+ # The trade, plainly: a Retry-After past this makes the 429 surface as an
21
+ # error where it used to be waited out, which is most of them -- the value
22
+ # being the remainder of the minute window. Absorbing them is `RateLimiter`'s
23
+ # job, metering each endpoint inside its budget before Pylon has to answer
24
+ # 429 at all; this retry is the backstop for the traffic the limiter cannot
25
+ # see, and a backstop may cost less than what it protects.
26
+ DEFAULT_MAX_INTERVAL = 12
27
+
28
+ STATUSES = [429, 502, 503, 504].freeze
29
+
30
+ # faraday-retry's defaults plus ConnectionFailed: a dropped connection is
31
+ # exactly the transient failure a resilient client should absorb, and it is
32
+ # not retried out of the box.
33
+ EXCEPTIONS = [
34
+ Errno::ETIMEDOUT, 'Timeout::Error', Faraday::TimeoutError,
35
+ Faraday::RetriableResponse, Faraday::ConnectionFailed
36
+ ].freeze
37
+
38
+ # The verbs that change nothing, so any transient failure is worth another
39
+ # attempt. Narrower than faraday-retry's idempotent default: a 502 or a
40
+ # dropped connection on the way back from a DELETE Pylon did perform is
41
+ # replayed into a 404, which the write path then surfaces as a deletion that
42
+ # failed when it landed.
43
+ #
44
+ # A 429 stays safe to retry on any verb, Pylon having rejected the request
45
+ # before processing it, and travels through retry_if rather than through this
46
+ # list: faraday-retry ORs the two, so methods can only widen the set, never
47
+ # restrict it.
48
+ RETRYABLE_METHODS = %i[get head options].freeze
49
+ RETRY_IF = ->(env, _exception) { env[:status] == 429 }
50
+
51
+ # The same mechanism as DEFAULT_MAX_INTERVAL, tightened for a call that must
52
+ # not hold the boot: what a request may wait once the agent is serving is
53
+ # more than an operator watching Rails come up should pay per object type.
54
+ BOOT_MAX_INTERVAL = 2
55
+
56
+ BACKOFF_FACTOR = 2
57
+
58
+ attr_reader :max_retries, :interval, :max_interval
59
+
60
+ # One retry rather than none, for what is read once and never revisited: a
61
+ # transient failure there costs its result for the whole life of the process,
62
+ # and half a second absorbs the hiccup without waiting a 429 out.
63
+ def self.boot
64
+ new(max_retries: 1, interval: 0.5, max_interval: BOOT_MAX_INTERVAL)
65
+ end
66
+
67
+ def initialize(max_retries: 3, interval: 0.5, max_interval: DEFAULT_MAX_INTERVAL)
68
+ @max_retries = max_retries
69
+ @interval = interval
70
+ @max_interval = max_interval
71
+ end
72
+
73
+ def to_faraday_options
74
+ {
75
+ max: @max_retries,
76
+ interval: @interval,
77
+ max_interval: @max_interval,
78
+ backoff_factor: BACKOFF_FACTOR,
79
+ retry_statuses: STATUSES,
80
+ exceptions: EXCEPTIONS,
81
+ methods: RETRYABLE_METHODS,
82
+ retry_if: RETRY_IF
83
+ }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,203 @@
1
+ module ForestAdminDatasourcePylon
2
+ module Schema
3
+ # Turns the custom fields an organization defined in Pylon into columns, as
4
+ # entries shaped `{ column_name:, schema:, multi_value: }` — what
5
+ # `add_custom_fields` registers on a collection, and what the payload
6
+ # builder writes a value back through.
7
+ #
8
+ # `column_name` is the Pylon slug verbatim, and there is no second key
9
+ # carrying it: the slug is both what a read payload indexes the values by and
10
+ # what a search filter sends as `field`, so renaming the column would add a
11
+ # mapping to keep in step for nothing.
12
+ #
13
+ # Pylon defines custom fields per object type, and asks for that type on
14
+ # every call: `issue`, `account` and `contact` are the three this datasource
15
+ # has a collection for — it also exposes `task`, `project`, `meeting` and
16
+ # `opportunity`, while users and teams carry no custom field at all.
17
+ class CustomFieldsIntrospector
18
+ ColumnSchema = ForestAdminDatasourceToolkit::Schema::ColumnSchema
19
+ Maps = Query::OperatorMaps
20
+
21
+ # A type absent from this table is skipped rather than guessed at: a column
22
+ # whose Forest type does not match what Pylon holds would filter and
23
+ # display wrong, which is worse than not being there.
24
+ #
25
+ # `user` holds a Pylon user id, kept a String rather than turned into a
26
+ # relation to PylonUser: a custom field is registered after the relations
27
+ # are declared, and a foreign key the operator can read is what this story
28
+ # promises.
29
+ PYLON_TO_COLUMN_TYPE = {
30
+ 'text' => 'String',
31
+ 'url' => 'String',
32
+ 'user' => 'String',
33
+ 'number' => 'Number',
34
+ 'decimal' => 'Number',
35
+ 'boolean' => 'Boolean',
36
+ 'date' => 'Dateonly',
37
+ 'datetime' => 'Date',
38
+ 'select' => 'Enum',
39
+ 'multiselect' => 'Json'
40
+ }.freeze
41
+
42
+ # The types Pylon writes back through `values` rather than `value`.
43
+ MULTI_VALUE_TYPES = %w[multiselect].freeze
44
+
45
+ BASE_OPS = (Maps::EQUALITY.keys + Maps::PRESENCE.keys).freeze
46
+
47
+ # A date drops the membership operators on the way, `Rules` granting a DATE
48
+ # or a DATEONLY column no array operator -- the hazard already documented
49
+ # for MEMBERSHIP in `operator_maps.rb`. Native date columns declare the
50
+ # comparisons alone for the same reason.
51
+ #
52
+ # Dropping them is not what keeps `in` out of the UI, though, and nothing
53
+ # here can: `OperatorsEquivalenceCollectionDecorator` republishes it from
54
+ # the `equal` this set declares, the IN transform depending on EQUAL for
55
+ # every column type. A DATEONLY also gets `after_x_hours_ago` /
56
+ # `before_x_hours_ago` republished from the comparisons, `Times.compare`
57
+ # deriving them for that type where `Rules` refuses them. The validator
58
+ # then rejects all three, so the operator is offered a date filter the
59
+ # agent answers with a 400. The contradiction is the toolkit's to settle
60
+ # and is tracked as PRD-989; the set below is what Pylon accepts, which is
61
+ # the only question this table can answer.
62
+ TIME_OPS = (BASE_OPS - Maps::MEMBERSHIP.keys + Maps::TIME.keys).freeze
63
+
64
+ # Drawn from `CUSTOM_FIELD_OPS`, the set every search endpoint accepts on a
65
+ # custom field, so a collection's clamp has nothing to drop.
66
+ #
67
+ # A Number gets no comparison: Pylon documents `time_is_after` /
68
+ # `time_is_before` for the bare comparisons and nothing else, so a numeric
69
+ # range would travel as a time filter. A multiselect gets nothing at all —
70
+ # its membership operators are not part of what a custom field accepts.
71
+ OPERATORS = {
72
+ 'String' => (BASE_OPS + Maps::FULL_TEXT.keys).freeze,
73
+ 'Enum' => BASE_OPS,
74
+ 'Number' => BASE_OPS,
75
+ 'Boolean' => BASE_OPS,
76
+ 'Date' => TIME_OPS,
77
+ 'Dateonly' => TIME_OPS,
78
+ 'Json' => [].freeze
79
+ }.freeze
80
+
81
+ def initialize(client)
82
+ @client = client
83
+ @unreachable = false
84
+ end
85
+
86
+ def issue_custom_fields = introspect('issue')
87
+ def account_custom_fields = introspect('account')
88
+ def contact_custom_fields = introspect('contact')
89
+
90
+ private
91
+
92
+ # One object type per call, three of them, all of it in front of a Rails
93
+ # boot the operator sits through. The first failure stands for the rest:
94
+ # a Pylon that is down, or a token missing the permission, fails the two
95
+ # that follow the same way, and each is bounded per request rather than
96
+ # across the three — so trying them anyway spends the bound three times to
97
+ # learn what the first one already said.
98
+ def introspect(object_type)
99
+ return [] if @unreachable
100
+
101
+ definitions = @client.fetch_custom_fields(object_type)
102
+ return give_up(object_type) if definitions.nil?
103
+
104
+ definitions.filter_map { |raw| build_entry(raw, object_type) }
105
+ end
106
+
107
+ def give_up(object_type)
108
+ @unreachable = true
109
+ ForestAdminDatasourcePylon.logger.warn(
110
+ "[forest_admin_datasource_pylon] Custom fields could not be read for #{object_type}; " \
111
+ 'the object types after it are left unread rather than held against the same failure. ' \
112
+ 'The datasource boots on the native schema, without the custom columns.'
113
+ )
114
+ []
115
+ end
116
+
117
+ def build_entry(raw, object_type)
118
+ return nil unless raw.is_a?(Hash)
119
+
120
+ slug = raw['slug'].to_s
121
+ return nil if slug.empty?
122
+
123
+ column_type = PYLON_TO_COLUMN_TYPE[raw['type']]
124
+ return warn_unknown_type(raw, slug, object_type) if column_type.nil?
125
+
126
+ { column_name: slug, schema: build_schema(raw, column_type),
127
+ multi_value: MULTI_VALUE_TYPES.include?(raw['type']) }
128
+ end
129
+
130
+ # A custom field is writable when Pylon says it is: it flags the ones
131
+ # synced from an app or an integration, which its own endpoints refuse.
132
+ # Nothing is sortable -- no Pylon endpoint takes a sort parameter -- and
133
+ # nothing is groupable: one column left groupable turns `supportGroups` on
134
+ # for the whole collection, and the group-by the UI then offers errors.
135
+ def build_schema(raw, column_type)
136
+ opts = { column_type: column_type,
137
+ filter_operators: OPERATORS.fetch(column_type, []),
138
+ is_read_only: !writable_definition?(raw),
139
+ is_sortable: false,
140
+ is_groupable: false }
141
+
142
+ column_type == 'Enum' ? enum_schema(raw, opts) : ColumnSchema.new(**opts)
143
+ end
144
+
145
+ # Pylon reads a select back — and filters it — as the slug of the option,
146
+ # never as its label, so those are the values the column advertises.
147
+ #
148
+ # Forest refuses an Enum carrying no value: a select whose options were all
149
+ # removed falls back to String, so the column still shows what it holds.
150
+ #
151
+ # Read-only there whatever Pylon says of the field, unlike every other
152
+ # fallback here: a select is written as the slug of one of its options, so
153
+ # a free-text editor on one offers the operator no value the endpoint would
154
+ # accept. Showing what it holds is the whole of what this can do.
155
+ def enum_schema(raw, opts)
156
+ values = option_slugs(raw)
157
+ return ColumnSchema.new(**opts, enum_values: values) unless values.empty?
158
+
159
+ ColumnSchema.new(**opts, column_type: 'String',
160
+ filter_operators: OPERATORS.fetch('String'), is_read_only: true)
161
+ end
162
+
163
+ def option_slugs(raw)
164
+ metadata = raw['select_metadata']
165
+ options = metadata.is_a?(Hash) ? metadata['options'] : nil
166
+
167
+ Array(options).filter_map do |option|
168
+ option['slug'] if option.is_a?(Hash) && !option['slug'].to_s.empty?
169
+ end
170
+ end
171
+
172
+ # Only an explicit `false` opens a custom field to writes. A definition
173
+ # carrying no flag at all is left read-only and reported: this datasource
174
+ # advertises nothing an endpoint would refuse, and reading the absence as
175
+ # "writable" would turn every field synced from an app into an editor whose
176
+ # every save Pylon rejects -- where reading it as "read-only" costs the
177
+ # capability and says so once per boot.
178
+ def writable_definition?(raw)
179
+ return true if raw['is_read_only'] == false
180
+ return false if raw['is_read_only'] == true
181
+
182
+ warn_unflagged_writability(raw)
183
+ false
184
+ end
185
+
186
+ def warn_unflagged_writability(raw)
187
+ ForestAdminDatasourcePylon.logger.warn(
188
+ "[forest_admin_datasource_pylon] Custom field '#{raw["slug"]}' carries no 'is_read_only' flag; " \
189
+ 'leaving it read-only. Pylon refuses a write on the fields it syncs from an app or an integration, ' \
190
+ 'and nothing here can tell this one apart from those without the flag.'
191
+ )
192
+ end
193
+
194
+ def warn_unknown_type(raw, slug, object_type)
195
+ ForestAdminDatasourcePylon.logger.warn(
196
+ "[forest_admin_datasource_pylon] Custom field '#{slug}' on #{object_type} has type " \
197
+ "#{raw["type"].inspect}, which this datasource cannot map to a Forest column; skipping."
198
+ )
199
+ nil
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,21 @@
1
+ module ForestAdminDatasourcePylon
2
+ # Holds a request until its endpoint has budget. A middleware rather than a
3
+ # call in each client method: there is one code path for every request here,
4
+ # where the client has twenty, and this one also catches the requests the
5
+ # client never issues itself — the replays `retry` performs.
6
+ class Throttle < Faraday::Middleware
7
+ def initialize(app, limiter:, base_path: '')
8
+ super(app)
9
+ @limiter = limiter
10
+ @base_path = base_path.to_s
11
+ end
12
+
13
+ # Faraday hands over the path of the resolved url, so a base url mounted
14
+ # under a subpath carries that prefix and no rule matches. It comes off
15
+ # before the limiter sees the path.
16
+ def call(env)
17
+ @limiter.acquire(env.method, env.url.path.delete_prefix(@base_path))
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module ForestAdminDatasourcePylon
2
+ VERSION = "1.41.0"
3
+ end
@@ -0,0 +1,70 @@
1
+ require_relative 'forest_admin_datasource_pylon/version'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'set'
5
+ require 'uri'
6
+ require 'zeitwerk'
7
+ require 'faraday'
8
+ require 'faraday/retry'
9
+ require 'forest_admin_datasource_toolkit'
10
+
11
+ loader = Zeitwerk::Loader.for_gem
12
+ loader.setup
13
+
14
+ module ForestAdminDatasourcePylon
15
+ class Error < StandardError; end
16
+ class ConfigurationError < Error; end
17
+
18
+ # A filter Pylon cannot express. It descends from the toolkit's ValidationError
19
+ # rather than from the package's own Error so the agent answers 400 carrying
20
+ # the message instead of a 500 "Unexpected error": every one of these names a
21
+ # condition the operator set and can change, and the message is the only place
22
+ # they learn which one.
23
+ class UnsupportedOperatorError < ForestAdminDatasourceToolkit::Exceptions::ValidationError; end
24
+
25
+ # The three write errors below descend from ValidationError for that same
26
+ # reason: each names something the operator did and can undo.
27
+
28
+ # A verb Pylon's API has no endpoint for, a field it only accepts in the other
29
+ # direction, or a write reaching more records than one pass may cover.
30
+ class UnsupportedWriteError < ForestAdminDatasourceToolkit::Exceptions::ValidationError; end
31
+
32
+ # A write Pylon performed on some of its records and then failed on: one
33
+ # record is one request, so the ones before the failure stay written, and a
34
+ # retry of the whole selection would write them a second time.
35
+ class PartialWriteError < ForestAdminDatasourceToolkit::Exceptions::ValidationError; end
36
+
37
+ # A write Pylon itself refused, carrying the reason it gave — the likeliest
38
+ # way a write fails. Only its 4xx travels this way: a 5xx or a dropped
39
+ # connection is not the operator's to act on and stays the APIError it was.
40
+ class WriteRejectedError < ForestAdminDatasourceToolkit::Exceptions::ValidationError; end
41
+
42
+ # Raised when a Pylon API call fails. Carries the HTTP status and the
43
+ # (parsed) response body so callers — smart actions in particular — can
44
+ # surface Pylon's own validation message instead of a generic string.
45
+ class APIError < Error
46
+ attr_reader :status, :body
47
+
48
+ def initialize(message, status: nil, body: nil)
49
+ super(message)
50
+ @status = status
51
+ @body = body
52
+ end
53
+ end
54
+
55
+ class << self
56
+ attr_writer :logger
57
+
58
+ def logger
59
+ @logger ||= default_logger
60
+ end
61
+
62
+ private
63
+
64
+ def default_logger
65
+ return Rails.logger if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
66
+
67
+ Logger.new($stderr).tap { |l| l.progname = 'forest_admin_datasource_pylon' }
68
+ end
69
+ end
70
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forest_admin_datasource_pylon
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.41.0
5
+ platform: ruby
6
+ authors:
7
+ - Forest Admin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-09-01 00:00:00.000000000 Z
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: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-retry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
69
+ description: Surface Pylon issues, accounts, contacts, users and teams as Forest Admin
70
+ collections.
71
+ email:
72
+ - contact@forestadmin.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".rspec"
78
+ - README.md
79
+ - Rakefile
80
+ - forest_admin_datasource_pylon.gemspec
81
+ - lib/forest_admin_datasource_pylon.rb
82
+ - lib/forest_admin_datasource_pylon/client.rb
83
+ - lib/forest_admin_datasource_pylon/client/writes.rb
84
+ - lib/forest_admin_datasource_pylon/collections/account.rb
85
+ - lib/forest_admin_datasource_pylon/collections/account/api_filters.rb
86
+ - lib/forest_admin_datasource_pylon/collections/account/schema_definition.rb
87
+ - lib/forest_admin_datasource_pylon/collections/account/serializer.rb
88
+ - lib/forest_admin_datasource_pylon/collections/base_collection.rb
89
+ - lib/forest_admin_datasource_pylon/collections/contact.rb
90
+ - lib/forest_admin_datasource_pylon/collections/contact/api_filters.rb
91
+ - lib/forest_admin_datasource_pylon/collections/contact/schema_definition.rb
92
+ - lib/forest_admin_datasource_pylon/collections/contact/serializer.rb
93
+ - lib/forest_admin_datasource_pylon/collections/cursor_collection.rb
94
+ - lib/forest_admin_datasource_pylon/collections/fetch_all_collection.rb
95
+ - lib/forest_admin_datasource_pylon/collections/issue.rb
96
+ - lib/forest_admin_datasource_pylon/collections/issue/api_filters.rb
97
+ - lib/forest_admin_datasource_pylon/collections/issue/id_lookup_reader.rb
98
+ - lib/forest_admin_datasource_pylon/collections/issue/messages_embedder.rb
99
+ - lib/forest_admin_datasource_pylon/collections/issue/schema_definition.rb
100
+ - lib/forest_admin_datasource_pylon/collections/issue/serializer.rb
101
+ - lib/forest_admin_datasource_pylon/collections/record_serialization.rb
102
+ - lib/forest_admin_datasource_pylon/collections/relation_embedder.rb
103
+ - lib/forest_admin_datasource_pylon/collections/team.rb
104
+ - lib/forest_admin_datasource_pylon/collections/user.rb
105
+ - lib/forest_admin_datasource_pylon/collections/writes.rb
106
+ - lib/forest_admin_datasource_pylon/configuration.rb
107
+ - lib/forest_admin_datasource_pylon/datasource.rb
108
+ - lib/forest_admin_datasource_pylon/issue_enums.rb
109
+ - lib/forest_admin_datasource_pylon/pagination/cursor_walker.rb
110
+ - lib/forest_admin_datasource_pylon/plugins/close_issue.rb
111
+ - lib/forest_admin_datasource_pylon/plugins/close_issue/messages.rb
112
+ - lib/forest_admin_datasource_pylon/plugins/create_issue_with_notification.rb
113
+ - lib/forest_admin_datasource_pylon/plugins/create_issue_with_notification/form_builder.rb
114
+ - lib/forest_admin_datasource_pylon/plugins/create_issue_with_notification/payload.rb
115
+ - lib/forest_admin_datasource_pylon/plugins/issue_targets.rb
116
+ - lib/forest_admin_datasource_pylon/query/condition_tree_translator.rb
117
+ - lib/forest_admin_datasource_pylon/query/filter_value.rb
118
+ - lib/forest_admin_datasource_pylon/query/operator_maps.rb
119
+ - lib/forest_admin_datasource_pylon/rate_limiter.rb
120
+ - lib/forest_admin_datasource_pylon/rate_limits.rb
121
+ - lib/forest_admin_datasource_pylon/retry_policy.rb
122
+ - lib/forest_admin_datasource_pylon/schema/custom_fields_introspector.rb
123
+ - lib/forest_admin_datasource_pylon/throttle.rb
124
+ - lib/forest_admin_datasource_pylon/version.rb
125
+ homepage: https://www.forestadmin.com
126
+ licenses:
127
+ - GPL-3.0
128
+ metadata:
129
+ homepage_uri: https://www.forestadmin.com
130
+ source_code_uri: https://github.com/ForestAdmin/agent-ruby
131
+ changelog_uri: https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md
132
+ rubygems_mfa_required: 'false'
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 3.0.0
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.4.20
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: Pylon datasource for Forest Admin Ruby agent.
152
+ test_files: []