flodesk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +101 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +351 -0
  5. data/Rakefile +12 -0
  6. data/lib/flodesk/auth.rb +32 -0
  7. data/lib/flodesk/client.rb +84 -0
  8. data/lib/flodesk/coercion.rb +69 -0
  9. data/lib/flodesk/connection.rb +179 -0
  10. data/lib/flodesk/enums.rb +42 -0
  11. data/lib/flodesk/errors.rb +139 -0
  12. data/lib/flodesk/instrumentation.rb +35 -0
  13. data/lib/flodesk/objects/batch_item_error.rb +29 -0
  14. data/lib/flodesk/objects/batch_result.rb +44 -0
  15. data/lib/flodesk/objects/campaign.rb +28 -0
  16. data/lib/flodesk/objects/custom_field.rb +21 -0
  17. data/lib/flodesk/objects/page.rb +72 -0
  18. data/lib/flodesk/objects/segment.rb +36 -0
  19. data/lib/flodesk/objects/subscriber.rb +39 -0
  20. data/lib/flodesk/objects/webhook.rb +24 -0
  21. data/lib/flodesk/objects/workflow.rb +22 -0
  22. data/lib/flodesk/rails/railtie.rb +19 -0
  23. data/lib/flodesk/rails.rb +8 -0
  24. data/lib/flodesk/rate_limit.rb +35 -0
  25. data/lib/flodesk/redaction.rb +46 -0
  26. data/lib/flodesk/resources/base.rb +122 -0
  27. data/lib/flodesk/resources/campaigns.rb +121 -0
  28. data/lib/flodesk/resources/custom_fields.rb +47 -0
  29. data/lib/flodesk/resources/segments.rb +49 -0
  30. data/lib/flodesk/resources/subscribers.rb +239 -0
  31. data/lib/flodesk/resources/webhooks.rb +93 -0
  32. data/lib/flodesk/resources/workflows.rb +75 -0
  33. data/lib/flodesk/response.rb +35 -0
  34. data/lib/flodesk/retry_policy.rb +46 -0
  35. data/lib/flodesk/test_helpers.rb +152 -0
  36. data/lib/flodesk/version.rb +5 -0
  37. data/lib/flodesk/webhooks/event.rb +78 -0
  38. data/lib/flodesk/webhooks/handler.rb +167 -0
  39. data/lib/flodesk/webhooks/verification.rb +61 -0
  40. data/lib/flodesk.rb +59 -0
  41. data/lib/generators/flodesk/install_generator.rb +61 -0
  42. data/lib/generators/flodesk/templates/initializer.rb.tt +28 -0
  43. data/sig/flodesk/client.rbs +91 -0
  44. data/sig/flodesk/errors.rbs +51 -0
  45. data/sig/flodesk/objects.rbs +137 -0
  46. data/sig/flodesk/resources.rbs +129 -0
  47. data/sig/flodesk/webhooks.rbs +57 -0
  48. data/sig/flodesk.rbs +61 -0
  49. metadata +97 -0
@@ -0,0 +1,137 @@
1
+ # Immutable response value objects.
2
+ #
3
+ # Every object exposes `raw` / `#to_h` returning the payload exactly as the API
4
+ # sent it, so a field Flodesk adds is reachable without a gem release.
5
+ module Flodesk
6
+ class Subscriber
7
+ attr_reader id: String?
8
+ attr_reader status: enum_value
9
+ attr_reader email: String?
10
+ attr_reader source: enum_value
11
+ attr_reader first_name: String?
12
+ attr_reader last_name: String?
13
+ attr_reader segments: Array[Segment]
14
+ attr_reader custom_fields: Hash[String, String?]
15
+ attr_reader optin_ip: String?
16
+ attr_reader optin_timestamp: timestamp
17
+ attr_reader created_at: timestamp
18
+ attr_reader raw: payload
19
+
20
+ def self.from: (payload?) -> Subscriber?
21
+ def self.members: () -> Array[Symbol]
22
+ def to_h: () -> payload
23
+ def active?: () -> bool
24
+ end
25
+
26
+ class Segment
27
+ attr_reader id: String?
28
+ attr_reader name: String?
29
+ attr_reader color: String?
30
+ attr_reader total_active_subscribers: Integer?
31
+ attr_reader created_at: timestamp
32
+ # "static" or "dynamic"; not symbolized, as the spec declares no enum.
33
+ attr_reader segment_type: String?
34
+ attr_reader raw: payload
35
+
36
+ def self.from: (payload?) -> Segment?
37
+ def self.members: () -> Array[Symbol]
38
+ def to_h: () -> payload
39
+ end
40
+
41
+ class CustomField
42
+ attr_reader key: String?
43
+ attr_reader label: String?
44
+ attr_reader raw: payload
45
+
46
+ def self.from: (payload?) -> CustomField?
47
+ def self.members: () -> Array[Symbol]
48
+ def to_h: () -> payload
49
+ end
50
+
51
+ class Workflow
52
+ attr_reader id: String?
53
+ attr_reader name: String?
54
+ attr_reader raw: payload
55
+
56
+ def self.from: (payload?) -> Workflow?
57
+ def self.members: () -> Array[Symbol]
58
+ def to_h: () -> payload
59
+ end
60
+
61
+ class Webhook
62
+ attr_reader id: String?
63
+ attr_reader post_url: String?
64
+ attr_reader events: Array[String]
65
+ attr_reader created_at: timestamp
66
+ attr_reader raw: payload
67
+
68
+ def self.from: (payload?) -> Webhook?
69
+ def self.members: () -> Array[Symbol]
70
+ def to_h: () -> payload
71
+ end
72
+
73
+ class Campaign
74
+ attr_reader id: String?
75
+ attr_reader name: String?
76
+ attr_reader subject: String?
77
+ attr_reader status: enum_value
78
+ attr_reader created_at: timestamp
79
+ attr_reader updated_at: timestamp
80
+ attr_reader raw: payload
81
+
82
+ def self.from: (payload?) -> Campaign?
83
+ def self.members: () -> Array[Symbol]
84
+ def to_h: () -> payload
85
+ end
86
+
87
+ # A single record's failure within a batch operation.
88
+ class BatchItemError
89
+ attr_reader index: Integer?
90
+ attr_reader email: String?
91
+ attr_reader id: String?
92
+ attr_reader code: String?
93
+ attr_reader message: String?
94
+ attr_reader raw: payload
95
+
96
+ def self.from: (payload?) -> BatchItemError?
97
+ def self.members: () -> Array[Symbol]
98
+ def to_h: () -> payload
99
+ end
100
+
101
+ # The outcome of a batch operation, which may be a partial success.
102
+ class BatchResult
103
+ attr_reader successes: Array[Subscriber]
104
+ attr_reader failures: Array[BatchItemError]
105
+ attr_reader raw: payload
106
+
107
+ def self.from: (payload?) -> BatchResult
108
+ def self.members: () -> Array[Symbol]
109
+ def success?: () -> bool
110
+ def size: () -> Integer
111
+ def failed_emails: () -> Array[String]
112
+ def to_h: () -> payload
113
+ end
114
+
115
+ # One page of a list response.
116
+ class Page
117
+ include Enumerable[untyped]
118
+
119
+ attr_reader items: Array[untyped]
120
+ attr_reader page: Integer?
121
+ attr_reader per_page: Integer?
122
+ attr_reader total_pages: Integer?
123
+ attr_reader total_items: Integer?
124
+ attr_reader raw: payload
125
+ attr_reader fetcher: ^(Integer) -> Page?
126
+
127
+ def self.from: (payload?, klass: untyped, ?fetcher: (^(Integer) -> Page?)?) -> Page
128
+ def self.members: () -> Array[Symbol]
129
+ def each: () { (untyped) -> void } -> void
130
+ | () -> Enumerator[untyped, void]
131
+ def empty?: () -> bool
132
+ def more_pages?: () -> bool
133
+ def to_h: () -> payload
134
+ def auto_paging_each: () { (untyped) -> void } -> void
135
+ | () -> Enumerator[untyped, void]
136
+ end
137
+ end
@@ -0,0 +1,129 @@
1
+ module Flodesk
2
+ module Resources
3
+ # Shared behavior for resource namespaces.
4
+ class Base
5
+ MAX_PER_PAGE: Integer
6
+ UNRESERVED: Regexp
7
+
8
+ def initialize: (Client) -> void
9
+ end
10
+
11
+ class Subscribers < Base
12
+ PATH: String
13
+ MAX_SEGMENTS: Integer
14
+ MAX_BATCH_SIZE: Integer
15
+
16
+ def list: (
17
+ ?page: Integer?, ?per_page: Integer?,
18
+ ?status: (Symbol | String)?, ?segment_id: String?
19
+ ) -> Page
20
+
21
+ def auto_paging_each: (
22
+ ?page: Integer?, ?per_page: Integer?,
23
+ ?status: (Symbol | String)?, ?segment_id: String?
24
+ ) { (Subscriber) -> void } -> void
25
+ | (
26
+ ?page: Integer?, ?per_page: Integer?,
27
+ ?status: (Symbol | String)?, ?segment_id: String?
28
+ ) -> Enumerator[Subscriber, void]
29
+
30
+ def retrieve: (String) -> Subscriber?
31
+ def upsert: (**untyped) -> Subscriber?
32
+ def batch_upsert: (
33
+ Array[Hash[untyped, untyped]], ?raise_on_failure: bool
34
+ ) -> BatchResult
35
+ def add_to_segments: (String, Array[String]) -> Subscriber?
36
+ def remove_from_segments: (String, Array[String]) -> Subscriber?
37
+ def unsubscribe: (String) -> Subscriber?
38
+ end
39
+
40
+ class Segments < Base
41
+ PATH: String
42
+
43
+ def list: (?page: Integer?, ?per_page: Integer?) -> Page
44
+ def auto_paging_each: (?page: Integer?, ?per_page: Integer?) { (Segment) -> void } -> void
45
+ | (?page: Integer?, ?per_page: Integer?) -> Enumerator[Segment, void]
46
+ def retrieve: (String) -> Segment?
47
+ def create: (name: String, ?color: String?) -> Segment?
48
+ def colors: () -> untyped
49
+ end
50
+
51
+ class CustomFields < Base
52
+ PATH: String
53
+
54
+ def list: (?page: Integer?, ?per_page: Integer?) -> Page
55
+ def auto_paging_each: (?page: Integer?, ?per_page: Integer?) { (CustomField) -> void } -> void
56
+ | (?page: Integer?, ?per_page: Integer?) -> Enumerator[CustomField, void]
57
+ def list_all: () -> Array[CustomField]
58
+ def create: (label: String) -> CustomField?
59
+ end
60
+
61
+ class Workflows < Base
62
+ PATH: String
63
+ # This endpoint alone spells its page-size parameter `perPage`.
64
+ PER_PAGE_KEY: String
65
+
66
+ def list: (
67
+ ?page: Integer?, ?per_page: Integer?,
68
+ ?statuses: (String | Symbol | Array[String | Symbol])?
69
+ ) -> Page
70
+
71
+ def auto_paging_each: (
72
+ ?page: Integer?, ?per_page: Integer?,
73
+ ?statuses: (String | Symbol | Array[String | Symbol])?
74
+ ) { (Workflow) -> void } -> void
75
+ | (
76
+ ?page: Integer?, ?per_page: Integer?,
77
+ ?statuses: (String | Symbol | Array[String | Symbol])?
78
+ ) -> Enumerator[Workflow, void]
79
+
80
+ # Returns nil: the endpoint responds 204.
81
+ def add_subscriber: (String, ?id: String?, ?email: String?) -> nil
82
+ # Returns nil: the endpoint responds 204.
83
+ def remove_subscriber: (String, String) -> nil
84
+ end
85
+
86
+ class Webhooks < Base
87
+ PATH: String
88
+
89
+ def list: (?page: Integer?, ?per_page: Integer?) -> Page
90
+ def auto_paging_each: (?page: Integer?, ?per_page: Integer?) { (Webhook) -> void } -> void
91
+ | (?page: Integer?, ?per_page: Integer?) -> Enumerator[Webhook, void]
92
+ def retrieve: (String) -> Webhook?
93
+ def create: (
94
+ name: String, post_url: String, events: (String | Array[String])
95
+ ) -> Webhook?
96
+ def update: (
97
+ String, ?name: String?, ?post_url: String?,
98
+ ?events: (String | Array[String])?
99
+ ) -> Webhook?
100
+ # Returns nil: the endpoint responds 204.
101
+ def delete: (String) -> nil
102
+ end
103
+
104
+ class Campaigns < Base
105
+ PATH: String
106
+ # This endpoint's filters are PascalCase, unlike anything else in the API.
107
+ FILTER_KEYS: Hash[Symbol, String]
108
+
109
+ def list: (?page: Integer?, ?per_page: Integer?, **untyped) -> Page
110
+ def auto_paging_each: (
111
+ ?page: Integer?, ?per_page: Integer?, **untyped
112
+ ) { (Campaign) -> void } -> void
113
+ | (?page: Integer?, ?per_page: Integer?, **untyped) -> Enumerator[Campaign, void]
114
+
115
+ # NEVER retried: a retry could send the campaign to the whole list twice.
116
+ def publish_canva: (
117
+ ?bundle_url: String?, ?title: String?, ?design_token: String?,
118
+ ?page_id: String?, ?campaign_id: String?
119
+ ) -> untyped
120
+
121
+ # NEVER retried, for the same reason as publish_canva.
122
+ def publish_studio: (
123
+ ?html: String?, ?title: String?, ?campaign_id: String?, ?asset_id: String?
124
+ ) -> untyped
125
+
126
+ def canva_design_state: () -> untyped
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,57 @@
1
+ module Flodesk
2
+ # Handling of inbound webhook deliveries.
3
+ #
4
+ # Flodesk signs nothing, so a verification strategy is mandatory.
5
+ module Webhooks
6
+ STRATEGIES: Array[Symbol]
7
+
8
+ class VerificationError < Error
9
+ def initialize: (?String) -> void
10
+ end
11
+
12
+ class InvalidPayloadError < Error
13
+ end
14
+
15
+ module Verification
16
+ MIN_TOKEN_LENGTH: Integer
17
+ TOKEN_BYTES: Integer
18
+
19
+ def self.generate_token: () -> String
20
+ def self.secure_compare: (String?, String?) -> bool
21
+ end
22
+
23
+ # A verified inbound webhook event.
24
+ class Event
25
+ attr_reader event_name: String?
26
+ attr_reader event_time: timestamp
27
+ attr_reader subscriber: Subscriber?
28
+ attr_reader segment: Segment?
29
+ attr_reader webhook_id: String?
30
+ attr_reader raw: payload
31
+
32
+ def self.from: (payload, ?subscriber: Subscriber?) -> Event
33
+ def self.members: () -> Array[Symbol]
34
+ def to_h: () -> payload
35
+ def known?: () -> bool
36
+ # SHA-256 of the composed identity, since events carry no unique id.
37
+ def dedupe_key: () -> String
38
+ def inspect: () -> String
39
+ def to_s: () -> String
40
+ end
41
+
42
+ class Handler
43
+ def self.generate_token: () -> String
44
+
45
+ def initialize: (
46
+ ?token: String?, ?verify: Symbol?, ?client: Client?
47
+ ) -> void
48
+
49
+ def call: (body: String?, ?token: String?) -> Event
50
+
51
+ # Returns a Rack triple: [status, headers, body].
52
+ def respond: (
53
+ body: String?, ?token: String?, ?on_error: (^(Exception) -> void)?
54
+ ) ?{ (Event) -> void } -> [Integer, Hash[String, String], Array[String]]
55
+ end
56
+ end
57
+ end
data/sig/flodesk.rbs ADDED
@@ -0,0 +1,61 @@
1
+ # Core module, configuration constants, and shared type aliases.
2
+ module Flodesk
3
+ VERSION: String
4
+ DEFAULT_BASE_URL: String
5
+ MAX_BACKOFF_SECONDS: Integer
6
+
7
+ # A parsed JSON object as received from the API.
8
+ type payload = Hash[String, untyped]
9
+
10
+ # An enum value: a Symbol when documented, otherwise the raw String.
11
+ type enum_value = Symbol | String | nil
12
+
13
+ # An ISO 8601 timestamp: a Time when parseable, otherwise the raw String.
14
+ type timestamp = Time | String | nil
15
+
16
+ # Closed enumerations documented by the API description.
17
+ module Enums
18
+ SUBSCRIBER_STATUSES: Array[String]
19
+ SUBSCRIBER_SOURCES: Array[String]
20
+ WORKFLOW_STATUSES: Array[String]
21
+ CAMPAIGN_STATUSES: Array[String]
22
+ WEBHOOK_EVENTS: Array[String]
23
+ end
24
+
25
+ module Coercion
26
+ def self.enum: (untyped, Array[String]) -> enum_value
27
+ def self.time: (untyped) -> timestamp
28
+ def self.array_of: (untyped, untyped) -> Array[untyped]
29
+ def self.string_hash: (untyped) -> Hash[String, String?]
30
+ def self.snapshot: (untyped) -> untyped
31
+ end
32
+
33
+ module Redaction
34
+ SENSITIVE_KEYS: Array[String]
35
+ PLACEHOLDER: String
36
+ EMAIL_IN_SEGMENT: Regexp
37
+
38
+ def self.path: (untyped) -> String
39
+ def self.payload: (untyped) -> untyped
40
+ end
41
+
42
+ module Instrumentation
43
+ EVENT_NAME: String
44
+
45
+ def self.available?: () -> bool
46
+ def self.request: (
47
+ method: Symbol, path: String, status: Integer,
48
+ duration: Float, attempt: Integer, rate_limit_remaining: Integer?
49
+ ) -> void
50
+ end
51
+
52
+ module RateLimit
53
+ class State
54
+ attr_reader limit: Integer?
55
+ attr_reader remaining: Integer?
56
+ end
57
+
58
+ def self.record: (Client, Response) -> void
59
+ def self.last: (Client) -> State?
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flodesk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jim
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ A dependency-free Ruby client for the Flodesk API, built for Rails apps.
14
+ Covers subscribers, segments, custom fields, workflows, webhooks and
15
+ campaigns, and hides the API's rough edges: non-uniform pagination
16
+ parameters, batch responses that report partial failure inside a 200, and
17
+ per-endpoint retry safety.
18
+ email:
19
+ - jim@mymat.yoga
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - CHANGELOG.md
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - lib/flodesk.rb
29
+ - lib/flodesk/auth.rb
30
+ - lib/flodesk/client.rb
31
+ - lib/flodesk/coercion.rb
32
+ - lib/flodesk/connection.rb
33
+ - lib/flodesk/enums.rb
34
+ - lib/flodesk/errors.rb
35
+ - lib/flodesk/instrumentation.rb
36
+ - lib/flodesk/objects/batch_item_error.rb
37
+ - lib/flodesk/objects/batch_result.rb
38
+ - lib/flodesk/objects/campaign.rb
39
+ - lib/flodesk/objects/custom_field.rb
40
+ - lib/flodesk/objects/page.rb
41
+ - lib/flodesk/objects/segment.rb
42
+ - lib/flodesk/objects/subscriber.rb
43
+ - lib/flodesk/objects/webhook.rb
44
+ - lib/flodesk/objects/workflow.rb
45
+ - lib/flodesk/rails.rb
46
+ - lib/flodesk/rails/railtie.rb
47
+ - lib/flodesk/rate_limit.rb
48
+ - lib/flodesk/redaction.rb
49
+ - lib/flodesk/resources/base.rb
50
+ - lib/flodesk/resources/campaigns.rb
51
+ - lib/flodesk/resources/custom_fields.rb
52
+ - lib/flodesk/resources/segments.rb
53
+ - lib/flodesk/resources/subscribers.rb
54
+ - lib/flodesk/resources/webhooks.rb
55
+ - lib/flodesk/resources/workflows.rb
56
+ - lib/flodesk/response.rb
57
+ - lib/flodesk/retry_policy.rb
58
+ - lib/flodesk/test_helpers.rb
59
+ - lib/flodesk/version.rb
60
+ - lib/flodesk/webhooks/event.rb
61
+ - lib/flodesk/webhooks/handler.rb
62
+ - lib/flodesk/webhooks/verification.rb
63
+ - lib/generators/flodesk/install_generator.rb
64
+ - lib/generators/flodesk/templates/initializer.rb.tt
65
+ - sig/flodesk.rbs
66
+ - sig/flodesk/client.rbs
67
+ - sig/flodesk/errors.rbs
68
+ - sig/flodesk/objects.rbs
69
+ - sig/flodesk/resources.rbs
70
+ - sig/flodesk/webhooks.rbs
71
+ homepage: https://github.com/mymat-yoga/flodesk-rb
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ allowed_push_host: https://rubygems.org
76
+ homepage_uri: https://github.com/mymat-yoga/flodesk-rb
77
+ source_code_uri: https://github.com/mymat-yoga/flodesk-rb
78
+ changelog_uri: https://github.com/mymat-yoga/flodesk-rb/blob/main/CHANGELOG.md
79
+ rubygems_mfa_required: 'true'
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.2.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 4.0.8
95
+ specification_version: 4
96
+ summary: Ruby client for the Flodesk API
97
+ test_files: []