pobo-sdk 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/Rakefile +8 -0
- data/lib/pobo/client.rb +168 -0
- data/lib/pobo/dto/blog.rb +89 -0
- data/lib/pobo/dto/category.rb +88 -0
- data/lib/pobo/dto/content.rb +48 -0
- data/lib/pobo/dto/import_result.rb +46 -0
- data/lib/pobo/dto/localized_string.rb +48 -0
- data/lib/pobo/dto/paginated_response.rb +43 -0
- data/lib/pobo/dto/parameter.rb +62 -0
- data/lib/pobo/dto/product.rb +103 -0
- data/lib/pobo/dto/webhook_payload.rb +27 -0
- data/lib/pobo/enum/language.rb +19 -0
- data/lib/pobo/enum/webhook_event.rb +14 -0
- data/lib/pobo/errors.rb +55 -0
- data/lib/pobo/version.rb +5 -0
- data/lib/pobo/webhook_handler.rb +52 -0
- data/lib/pobo.rb +34 -0
- data/pobo.gemspec +36 -0
- metadata +150 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class Category
|
|
6
|
+
attr_reader :id, :is_visible, :name, :url, :description, :seo_title,
|
|
7
|
+
:seo_description, :content, :images, :guid, :is_loaded,
|
|
8
|
+
:created_at, :updated_at
|
|
9
|
+
|
|
10
|
+
def initialize(
|
|
11
|
+
id:,
|
|
12
|
+
is_visible:,
|
|
13
|
+
name:,
|
|
14
|
+
url:,
|
|
15
|
+
description: nil,
|
|
16
|
+
seo_title: nil,
|
|
17
|
+
seo_description: nil,
|
|
18
|
+
content: nil,
|
|
19
|
+
images: [],
|
|
20
|
+
guid: nil,
|
|
21
|
+
is_loaded: nil,
|
|
22
|
+
created_at: nil,
|
|
23
|
+
updated_at: nil
|
|
24
|
+
)
|
|
25
|
+
@id = id
|
|
26
|
+
@is_visible = is_visible
|
|
27
|
+
@name = name
|
|
28
|
+
@url = url
|
|
29
|
+
@description = description
|
|
30
|
+
@seo_title = seo_title
|
|
31
|
+
@seo_description = seo_description
|
|
32
|
+
@content = content
|
|
33
|
+
@images = images
|
|
34
|
+
@guid = guid
|
|
35
|
+
@is_loaded = is_loaded
|
|
36
|
+
@created_at = created_at
|
|
37
|
+
@updated_at = updated_at
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from_hash(hash)
|
|
41
|
+
new(
|
|
42
|
+
id: hash["id"] || hash[:id],
|
|
43
|
+
is_visible: hash["is_visible"] || hash[:is_visible],
|
|
44
|
+
name: LocalizedString.from_hash(hash["name"] || hash[:name]),
|
|
45
|
+
url: LocalizedString.from_hash(hash["url"] || hash[:url]),
|
|
46
|
+
description: LocalizedString.from_hash(hash["description"] || hash[:description]),
|
|
47
|
+
seo_title: LocalizedString.from_hash(hash["seo_title"] || hash[:seo_title]),
|
|
48
|
+
seo_description: LocalizedString.from_hash(hash["seo_description"] || hash[:seo_description]),
|
|
49
|
+
content: Content.from_hash(hash["content"] || hash[:content]),
|
|
50
|
+
images: hash["images"] || hash[:images] || [],
|
|
51
|
+
guid: hash["guid"] || hash[:guid],
|
|
52
|
+
is_loaded: hash["is_loaded"] || hash[:is_loaded],
|
|
53
|
+
created_at: parse_time(hash["created_at"] || hash[:created_at]),
|
|
54
|
+
updated_at: parse_time(hash["updated_at"] || hash[:updated_at])
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_hash
|
|
59
|
+
data = {
|
|
60
|
+
"id" => @id,
|
|
61
|
+
"is_visible" => @is_visible,
|
|
62
|
+
"name" => @name&.to_hash,
|
|
63
|
+
"url" => @url&.to_hash
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
data["description"] = @description.to_hash if @description
|
|
67
|
+
data["seo_title"] = @seo_title.to_hash if @seo_title
|
|
68
|
+
data["seo_description"] = @seo_description.to_hash if @seo_description
|
|
69
|
+
data["images"] = @images unless @images.empty?
|
|
70
|
+
|
|
71
|
+
data
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
alias to_h to_hash
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def self.parse_time(value)
|
|
79
|
+
return nil if value.nil?
|
|
80
|
+
return value if value.is_a?(Time)
|
|
81
|
+
|
|
82
|
+
Time.parse(value)
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class Content
|
|
6
|
+
attr_reader :html, :marketplace
|
|
7
|
+
|
|
8
|
+
def initialize(html: {}, marketplace: {})
|
|
9
|
+
@html = html.transform_keys(&:to_s)
|
|
10
|
+
@marketplace = marketplace.transform_keys(&:to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_hash(hash)
|
|
14
|
+
return nil if hash.nil?
|
|
15
|
+
|
|
16
|
+
new(
|
|
17
|
+
html: hash["html"] || hash[:html] || {},
|
|
18
|
+
marketplace: hash["marketplace"] || hash[:marketplace] || {}
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def get_html(language)
|
|
23
|
+
@html[language.to_s]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_marketplace(language)
|
|
27
|
+
@marketplace[language.to_s]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def html_default
|
|
31
|
+
@html["default"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def marketplace_default
|
|
35
|
+
@marketplace["default"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_hash
|
|
39
|
+
{
|
|
40
|
+
"html" => @html,
|
|
41
|
+
"marketplace" => @marketplace
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias to_h to_hash
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class ImportResult
|
|
6
|
+
attr_reader :success, :imported, :updated, :skipped, :errors,
|
|
7
|
+
:values_imported, :values_updated
|
|
8
|
+
|
|
9
|
+
def initialize(
|
|
10
|
+
success:,
|
|
11
|
+
imported: 0,
|
|
12
|
+
updated: 0,
|
|
13
|
+
skipped: 0,
|
|
14
|
+
errors: [],
|
|
15
|
+
values_imported: nil,
|
|
16
|
+
values_updated: nil
|
|
17
|
+
)
|
|
18
|
+
@success = success
|
|
19
|
+
@imported = imported
|
|
20
|
+
@updated = updated
|
|
21
|
+
@skipped = skipped
|
|
22
|
+
@errors = errors
|
|
23
|
+
@values_imported = values_imported
|
|
24
|
+
@values_updated = values_updated
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_hash(hash)
|
|
28
|
+
new(
|
|
29
|
+
success: hash["success"] || hash[:success] || true,
|
|
30
|
+
imported: hash["imported"] || hash[:imported] || 0,
|
|
31
|
+
updated: hash["updated"] || hash[:updated] || 0,
|
|
32
|
+
skipped: hash["skipped"] || hash[:skipped] || 0,
|
|
33
|
+
errors: hash["errors"] || hash[:errors] || [],
|
|
34
|
+
values_imported: hash["values_imported"] || hash[:values_imported],
|
|
35
|
+
values_updated: hash["values_updated"] || hash[:values_updated]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def errors?
|
|
40
|
+
!@errors.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias has_errors? errors?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class LocalizedString
|
|
6
|
+
attr_reader :translations
|
|
7
|
+
|
|
8
|
+
def initialize(default_value = nil, translations: {})
|
|
9
|
+
@translations = translations.transform_keys(&:to_s)
|
|
10
|
+
@translations["default"] = default_value if default_value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.create(default_value)
|
|
14
|
+
new(default_value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_hash(hash)
|
|
18
|
+
return nil if hash.nil?
|
|
19
|
+
|
|
20
|
+
instance = new
|
|
21
|
+
hash.each do |key, value|
|
|
22
|
+
instance.translations[key.to_s] = value
|
|
23
|
+
end
|
|
24
|
+
instance
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def with_translation(language, value)
|
|
28
|
+
new_translations = @translations.dup
|
|
29
|
+
new_translations[language.to_s] = value
|
|
30
|
+
self.class.new(nil, translations: new_translations)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default
|
|
34
|
+
@translations["default"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get(language)
|
|
38
|
+
@translations[language.to_s]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_hash
|
|
42
|
+
@translations.dup
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias to_h to_hash
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class PaginatedResponse
|
|
6
|
+
attr_reader :data, :current_page, :per_page, :total
|
|
7
|
+
|
|
8
|
+
def initialize(data:, current_page:, per_page:, total:)
|
|
9
|
+
@data = data
|
|
10
|
+
@current_page = current_page
|
|
11
|
+
@per_page = per_page
|
|
12
|
+
@total = total
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_hash(hash, item_class)
|
|
16
|
+
items = (hash["data"] || hash[:data] || []).map do |item|
|
|
17
|
+
item_class.from_hash(item)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
meta = hash["meta"] || hash[:meta] || {}
|
|
21
|
+
|
|
22
|
+
new(
|
|
23
|
+
data: items,
|
|
24
|
+
current_page: meta["current_page"] || meta[:current_page] || 1,
|
|
25
|
+
per_page: meta["per_page"] || meta[:per_page] || 100,
|
|
26
|
+
total: meta["total"] || meta[:total] || 0
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def total_pages
|
|
31
|
+
return 0 if @per_page.zero?
|
|
32
|
+
|
|
33
|
+
(@total.to_f / @per_page).ceil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def more_pages?
|
|
37
|
+
@current_page < total_pages
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias has_more_pages? more_pages?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class ParameterValue
|
|
6
|
+
attr_reader :id, :value
|
|
7
|
+
|
|
8
|
+
def initialize(id:, value:)
|
|
9
|
+
@id = id
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_hash(hash)
|
|
14
|
+
new(
|
|
15
|
+
id: hash["id"] || hash[:id],
|
|
16
|
+
value: hash["value"] || hash[:value]
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_hash
|
|
21
|
+
{
|
|
22
|
+
"id" => @id,
|
|
23
|
+
"value" => @value
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias to_h to_hash
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Parameter
|
|
31
|
+
attr_reader :id, :name, :values
|
|
32
|
+
|
|
33
|
+
def initialize(id:, name:, values: [])
|
|
34
|
+
@id = id
|
|
35
|
+
@name = name
|
|
36
|
+
@values = values
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.from_hash(hash)
|
|
40
|
+
values = (hash["values"] || hash[:values] || []).map do |v|
|
|
41
|
+
ParameterValue.from_hash(v)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
new(
|
|
45
|
+
id: hash["id"] || hash[:id],
|
|
46
|
+
name: hash["name"] || hash[:name],
|
|
47
|
+
values: values
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_hash
|
|
52
|
+
{
|
|
53
|
+
"id" => @id,
|
|
54
|
+
"name" => @name,
|
|
55
|
+
"values" => @values.map(&:to_hash)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias to_h to_hash
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class Product
|
|
6
|
+
attr_reader :id, :is_visible, :name, :url, :short_description, :description,
|
|
7
|
+
:seo_title, :seo_description, :content, :images, :categories_ids,
|
|
8
|
+
:parameters_ids, :guid, :is_loaded, :categories, :created_at, :updated_at
|
|
9
|
+
|
|
10
|
+
def initialize(
|
|
11
|
+
id:,
|
|
12
|
+
is_visible:,
|
|
13
|
+
name:,
|
|
14
|
+
url:,
|
|
15
|
+
short_description: nil,
|
|
16
|
+
description: nil,
|
|
17
|
+
seo_title: nil,
|
|
18
|
+
seo_description: nil,
|
|
19
|
+
content: nil,
|
|
20
|
+
images: [],
|
|
21
|
+
categories_ids: [],
|
|
22
|
+
parameters_ids: [],
|
|
23
|
+
guid: nil,
|
|
24
|
+
is_loaded: nil,
|
|
25
|
+
categories: [],
|
|
26
|
+
created_at: nil,
|
|
27
|
+
updated_at: nil
|
|
28
|
+
)
|
|
29
|
+
@id = id
|
|
30
|
+
@is_visible = is_visible
|
|
31
|
+
@name = name
|
|
32
|
+
@url = url
|
|
33
|
+
@short_description = short_description
|
|
34
|
+
@description = description
|
|
35
|
+
@seo_title = seo_title
|
|
36
|
+
@seo_description = seo_description
|
|
37
|
+
@content = content
|
|
38
|
+
@images = images
|
|
39
|
+
@categories_ids = categories_ids
|
|
40
|
+
@parameters_ids = parameters_ids
|
|
41
|
+
@guid = guid
|
|
42
|
+
@is_loaded = is_loaded
|
|
43
|
+
@categories = categories
|
|
44
|
+
@created_at = created_at
|
|
45
|
+
@updated_at = updated_at
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.from_hash(hash)
|
|
49
|
+
new(
|
|
50
|
+
id: hash["id"] || hash[:id],
|
|
51
|
+
is_visible: hash["is_visible"] || hash[:is_visible],
|
|
52
|
+
name: LocalizedString.from_hash(hash["name"] || hash[:name]),
|
|
53
|
+
url: LocalizedString.from_hash(hash["url"] || hash[:url]),
|
|
54
|
+
short_description: LocalizedString.from_hash(hash["short_description"] || hash[:short_description]),
|
|
55
|
+
description: LocalizedString.from_hash(hash["description"] || hash[:description]),
|
|
56
|
+
seo_title: LocalizedString.from_hash(hash["seo_title"] || hash[:seo_title]),
|
|
57
|
+
seo_description: LocalizedString.from_hash(hash["seo_description"] || hash[:seo_description]),
|
|
58
|
+
content: Content.from_hash(hash["content"] || hash[:content]),
|
|
59
|
+
images: hash["images"] || hash[:images] || [],
|
|
60
|
+
categories_ids: hash["categories_ids"] || hash[:categories_ids] || [],
|
|
61
|
+
parameters_ids: hash["parameters_ids"] || hash[:parameters_ids] || [],
|
|
62
|
+
guid: hash["guid"] || hash[:guid],
|
|
63
|
+
is_loaded: hash["is_loaded"] || hash[:is_loaded],
|
|
64
|
+
categories: hash["categories"] || hash[:categories] || [],
|
|
65
|
+
created_at: parse_time(hash["created_at"] || hash[:created_at]),
|
|
66
|
+
updated_at: parse_time(hash["updated_at"] || hash[:updated_at])
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_hash
|
|
71
|
+
data = {
|
|
72
|
+
"id" => @id,
|
|
73
|
+
"is_visible" => @is_visible,
|
|
74
|
+
"name" => @name&.to_hash,
|
|
75
|
+
"url" => @url&.to_hash
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
data["short_description"] = @short_description.to_hash if @short_description
|
|
79
|
+
data["description"] = @description.to_hash if @description
|
|
80
|
+
data["seo_title"] = @seo_title.to_hash if @seo_title
|
|
81
|
+
data["seo_description"] = @seo_description.to_hash if @seo_description
|
|
82
|
+
data["images"] = @images unless @images.empty?
|
|
83
|
+
data["categories_ids"] = @categories_ids unless @categories_ids.empty?
|
|
84
|
+
data["parameters_ids"] = @parameters_ids unless @parameters_ids.empty?
|
|
85
|
+
|
|
86
|
+
data
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
alias to_h to_hash
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def self.parse_time(value)
|
|
94
|
+
return nil if value.nil?
|
|
95
|
+
return value if value.is_a?(Time)
|
|
96
|
+
|
|
97
|
+
Time.parse(value)
|
|
98
|
+
rescue ArgumentError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module DTO
|
|
5
|
+
class WebhookPayload
|
|
6
|
+
attr_reader :event, :timestamp, :eshop_id
|
|
7
|
+
|
|
8
|
+
def initialize(event:, timestamp:, eshop_id:)
|
|
9
|
+
@event = event
|
|
10
|
+
@timestamp = timestamp
|
|
11
|
+
@eshop_id = eshop_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_hash(hash)
|
|
15
|
+
timestamp = hash["timestamp"] || hash[:timestamp]
|
|
16
|
+
timestamp = Time.at(timestamp) if timestamp.is_a?(Integer)
|
|
17
|
+
timestamp = Time.parse(timestamp) if timestamp.is_a?(String)
|
|
18
|
+
|
|
19
|
+
new(
|
|
20
|
+
event: hash["event"] || hash[:event],
|
|
21
|
+
timestamp: timestamp,
|
|
22
|
+
eshop_id: hash["eshop_id"] || hash[:eshop_id]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module Language
|
|
5
|
+
DEFAULT = "default"
|
|
6
|
+
CS = "cs"
|
|
7
|
+
SK = "sk"
|
|
8
|
+
EN = "en"
|
|
9
|
+
DE = "de"
|
|
10
|
+
PL = "pl"
|
|
11
|
+
HU = "hu"
|
|
12
|
+
|
|
13
|
+
ALL = [DEFAULT, CS, SK, EN, DE, PL, HU].freeze
|
|
14
|
+
|
|
15
|
+
def self.valid?(code)
|
|
16
|
+
ALL.include?(code)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
module WebhookEvent
|
|
5
|
+
PRODUCTS_UPDATE = "products.update"
|
|
6
|
+
CATEGORIES_UPDATE = "categories.update"
|
|
7
|
+
|
|
8
|
+
ALL = [PRODUCTS_UPDATE, CATEGORIES_UPDATE].freeze
|
|
9
|
+
|
|
10
|
+
def self.valid?(event)
|
|
11
|
+
ALL.include?(event)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/pobo/errors.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pobo
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ApiError < Error
|
|
7
|
+
attr_reader :http_code, :response_body
|
|
8
|
+
|
|
9
|
+
def initialize(message, http_code: nil, response_body: nil)
|
|
10
|
+
@http_code = http_code
|
|
11
|
+
@response_body = response_body
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.unauthorized
|
|
16
|
+
new("Authorization token required", http_code: 401)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_response(http_code, body)
|
|
20
|
+
message = body.is_a?(Hash) ? (body["error"] || body["message"] || "API error") : "API error"
|
|
21
|
+
new(message, http_code: http_code, response_body: body)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ValidationError < Error
|
|
26
|
+
attr_reader :errors
|
|
27
|
+
|
|
28
|
+
def initialize(message, errors: {})
|
|
29
|
+
@errors = errors
|
|
30
|
+
super(message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.empty_payload
|
|
34
|
+
new("Payload cannot be empty")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.too_many_items(count, max)
|
|
38
|
+
new("Too many items: #{count} provided, maximum is #{max}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class WebhookError < Error
|
|
43
|
+
def self.missing_signature
|
|
44
|
+
new("Missing webhook signature")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.invalid_signature
|
|
48
|
+
new("Invalid webhook signature")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.invalid_payload
|
|
52
|
+
new("Invalid webhook payload")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/pobo/version.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Pobo
|
|
7
|
+
class WebhookHandler
|
|
8
|
+
SIGNATURE_HEADER = "X-Webhook-Signature"
|
|
9
|
+
|
|
10
|
+
def initialize(webhook_secret:)
|
|
11
|
+
@webhook_secret = webhook_secret
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Handle webhook from Rack request
|
|
15
|
+
def handle_request(request)
|
|
16
|
+
payload = request.body.read
|
|
17
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
|
18
|
+
|
|
19
|
+
signature = request.env["HTTP_X_WEBHOOK_SIGNATURE"] ||
|
|
20
|
+
request.get_header("HTTP_X_WEBHOOK_SIGNATURE") rescue nil
|
|
21
|
+
|
|
22
|
+
handle(payload: payload, signature: signature)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Handle webhook with raw payload and signature
|
|
26
|
+
def handle(payload:, signature:)
|
|
27
|
+
raise WebhookError.missing_signature if signature.nil? || signature.empty?
|
|
28
|
+
raise WebhookError.invalid_signature unless valid_signature?(payload, signature)
|
|
29
|
+
|
|
30
|
+
data = JSON.parse(payload)
|
|
31
|
+
DTO::WebhookPayload.from_hash(data)
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
raise WebhookError.invalid_payload
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def valid_signature?(payload, signature)
|
|
39
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", @webhook_secret, payload)
|
|
40
|
+
secure_compare(expected, signature)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def secure_compare(a, b)
|
|
44
|
+
return false unless a.bytesize == b.bytesize
|
|
45
|
+
|
|
46
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
47
|
+
rescue NoMethodError
|
|
48
|
+
# Fallback for older Ruby versions
|
|
49
|
+
a == b
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/pobo.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pobo/version"
|
|
4
|
+
require_relative "pobo/errors"
|
|
5
|
+
|
|
6
|
+
require_relative "pobo/enum/language"
|
|
7
|
+
require_relative "pobo/enum/webhook_event"
|
|
8
|
+
|
|
9
|
+
require_relative "pobo/dto/localized_string"
|
|
10
|
+
require_relative "pobo/dto/content"
|
|
11
|
+
require_relative "pobo/dto/product"
|
|
12
|
+
require_relative "pobo/dto/category"
|
|
13
|
+
require_relative "pobo/dto/blog"
|
|
14
|
+
require_relative "pobo/dto/parameter"
|
|
15
|
+
require_relative "pobo/dto/import_result"
|
|
16
|
+
require_relative "pobo/dto/paginated_response"
|
|
17
|
+
require_relative "pobo/dto/webhook_payload"
|
|
18
|
+
|
|
19
|
+
require_relative "pobo/client"
|
|
20
|
+
require_relative "pobo/webhook_handler"
|
|
21
|
+
|
|
22
|
+
module Pobo
|
|
23
|
+
class << self
|
|
24
|
+
# Convenience method to create a client
|
|
25
|
+
def client(api_token:, base_url: Client::DEFAULT_BASE_URL, timeout: Client::DEFAULT_TIMEOUT)
|
|
26
|
+
Client.new(api_token: api_token, base_url: base_url, timeout: timeout)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convenience method to create a webhook handler
|
|
30
|
+
def webhook_handler(webhook_secret:)
|
|
31
|
+
WebhookHandler.new(webhook_secret: webhook_secret)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/pobo.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/pobo/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "pobo-sdk"
|
|
7
|
+
spec.version = Pobo::VERSION
|
|
8
|
+
spec.authors = ["Pobo"]
|
|
9
|
+
spec.email = ["tomas@pobo.cz"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Official Ruby SDK for Pobo API V2"
|
|
12
|
+
spec.description = "Ruby SDK for Pobo API V2 - product content management and webhooks"
|
|
13
|
+
spec.homepage = "https://github.com/pobo-builder/ruby-sdk"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
(File.expand_path(f) == __FILE__) ||
|
|
24
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
|
|
29
|
+
spec.add_dependency "faraday", "~> 2.0"
|
|
30
|
+
spec.add_dependency "faraday-net_http", "~> 3.0"
|
|
31
|
+
|
|
32
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
34
|
+
spec.add_development_dependency "webmock", "~> 3.0"
|
|
35
|
+
spec.add_development_dependency "rubocop", "~> 1.0"
|
|
36
|
+
end
|