agave-client 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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +14 -0
  4. data/CODE_OF_CONDUCT.md +49 -0
  5. data/CONTRIBUTORS.md +34 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +29 -0
  8. data/README.md +41 -0
  9. data/Rakefile +7 -0
  10. data/agave-client.gemspec +63 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/exe/agave +10 -0
  14. data/lib/agave.rb +11 -0
  15. data/lib/agave/api_client.rb +97 -0
  16. data/lib/agave/api_error.rb +23 -0
  17. data/lib/agave/cli.rb +93 -0
  18. data/lib/agave/dump/dsl/add_to_data_file.rb +14 -0
  19. data/lib/agave/dump/dsl/create_data_file.rb +14 -0
  20. data/lib/agave/dump/dsl/create_post.rb +33 -0
  21. data/lib/agave/dump/dsl/directory.rb +30 -0
  22. data/lib/agave/dump/dsl/root.rb +37 -0
  23. data/lib/agave/dump/format.rb +28 -0
  24. data/lib/agave/dump/format/json.rb +18 -0
  25. data/lib/agave/dump/format/toml.rb +31 -0
  26. data/lib/agave/dump/format/yaml.rb +55 -0
  27. data/lib/agave/dump/operation/add_to_data_file.rb +41 -0
  28. data/lib/agave/dump/operation/create_data_file.rb +28 -0
  29. data/lib/agave/dump/operation/create_post.rb +34 -0
  30. data/lib/agave/dump/operation/directory.rb +34 -0
  31. data/lib/agave/dump/operation/root.rb +27 -0
  32. data/lib/agave/dump/runner.rb +47 -0
  33. data/lib/agave/dump/ssg_detector.rb +36 -0
  34. data/lib/agave/json_api_deserializer.rb +38 -0
  35. data/lib/agave/json_api_serializer.rb +145 -0
  36. data/lib/agave/local/entities_repo.rb +46 -0
  37. data/lib/agave/local/field_type/boolean.rb +12 -0
  38. data/lib/agave/local/field_type/color.rb +63 -0
  39. data/lib/agave/local/field_type/date.rb +12 -0
  40. data/lib/agave/local/field_type/date_time.rb +12 -0
  41. data/lib/agave/local/field_type/file.rb +65 -0
  42. data/lib/agave/local/field_type/float.rb +12 -0
  43. data/lib/agave/local/field_type/gallery.rb +23 -0
  44. data/lib/agave/local/field_type/global_seo.rb +56 -0
  45. data/lib/agave/local/field_type/image.rb +82 -0
  46. data/lib/agave/local/field_type/integer.rb +12 -0
  47. data/lib/agave/local/field_type/json.rb +13 -0
  48. data/lib/agave/local/field_type/lat_lon.rb +30 -0
  49. data/lib/agave/local/field_type/link.rb +12 -0
  50. data/lib/agave/local/field_type/links.rb +21 -0
  51. data/lib/agave/local/field_type/rich_text.rb +21 -0
  52. data/lib/agave/local/field_type/seo.rb +33 -0
  53. data/lib/agave/local/field_type/slug.rb +12 -0
  54. data/lib/agave/local/field_type/string.rb +12 -0
  55. data/lib/agave/local/field_type/text.rb +12 -0
  56. data/lib/agave/local/field_type/theme.rb +43 -0
  57. data/lib/agave/local/field_type/video.rb +77 -0
  58. data/lib/agave/local/item.rb +179 -0
  59. data/lib/agave/local/items_repo.rb +210 -0
  60. data/lib/agave/local/json_api_entity.rb +78 -0
  61. data/lib/agave/local/loader.rb +60 -0
  62. data/lib/agave/local/site.rb +73 -0
  63. data/lib/agave/paginator.rb +33 -0
  64. data/lib/agave/repo.rb +93 -0
  65. data/lib/agave/site/client.rb +24 -0
  66. data/lib/agave/upload/file.rb +92 -0
  67. data/lib/agave/upload/image.rb +8 -0
  68. data/lib/agave/utils/favicon_tags_builder.rb +85 -0
  69. data/lib/agave/utils/locale_value.rb +15 -0
  70. data/lib/agave/utils/meta_tags/article_modified_time.rb +15 -0
  71. data/lib/agave/utils/meta_tags/article_publisher.rb +18 -0
  72. data/lib/agave/utils/meta_tags/base.rb +55 -0
  73. data/lib/agave/utils/meta_tags/description.rb +24 -0
  74. data/lib/agave/utils/meta_tags/image.rb +35 -0
  75. data/lib/agave/utils/meta_tags/og_locale.rb +15 -0
  76. data/lib/agave/utils/meta_tags/og_site_name.rb +18 -0
  77. data/lib/agave/utils/meta_tags/og_type.rb +18 -0
  78. data/lib/agave/utils/meta_tags/robots.rb +14 -0
  79. data/lib/agave/utils/meta_tags/title.rb +46 -0
  80. data/lib/agave/utils/meta_tags/twitter_card.rb +14 -0
  81. data/lib/agave/utils/meta_tags/twitter_site.rb +18 -0
  82. data/lib/agave/utils/seo_tags_builder.rb +45 -0
  83. data/lib/agave/version.rb +4 -0
  84. data/lib/agave/watch/site_change_watcher.rb +37 -0
  85. metadata +504 -0
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Agave
3
+ module Local
4
+ module FieldType
5
+ class Text
6
+ def self.parse(value, _repo)
7
+ value
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ module Agave
2
+ module Local
3
+ module FieldType
4
+ class Theme
5
+ attr_reader :primary_color, :dark_color, :light_color, :accent_color
6
+
7
+ def self.parse(value, repo)
8
+ value && new(
9
+ value[:logo],
10
+ value[:primary_color],
11
+ value[:dark_color],
12
+ value[:light_color],
13
+ value[:accent_color],
14
+ repo
15
+ )
16
+ end
17
+
18
+ def initialize(logo, primary_color, dark_color, light_color, accent_color, repo)
19
+ @logo = logo
20
+ @primary_color = primary_color
21
+ @dark_color = dark_color
22
+ @light_color = light_color
23
+ @accent_color = accent_color
24
+ @repo = repo
25
+ end
26
+
27
+ def logo
28
+ @logo && File.parse(@logo, @repo)
29
+ end
30
+
31
+ def to_hash(*args)
32
+ {
33
+ primary_color: primary_color,
34
+ dark_color: dark_color,
35
+ light_color: light_color,
36
+ accent_color: accent_color,
37
+ logo: logo && logo.to_hash(*args)
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/hash/compact'
3
+
4
+ module Agave
5
+ module Local
6
+ module FieldType
7
+ class Video
8
+ attr_reader :url
9
+ attr_reader :thumbnail_url
10
+ attr_reader :title
11
+ attr_reader :width
12
+ attr_reader :height
13
+ attr_reader :provider
14
+ attr_reader :provider_url
15
+ attr_reader :provider_uid
16
+
17
+ def self.parse(value, _repo)
18
+ value && new(
19
+ value[:url],
20
+ value[:thumbnail_url],
21
+ value[:title],
22
+ value[:width],
23
+ value[:height],
24
+ value[:provider],
25
+ value[:provider_url],
26
+ value[:provider_uid]
27
+ )
28
+ end
29
+
30
+ def initialize(
31
+ url,
32
+ thumbnail_url,
33
+ title,
34
+ width,
35
+ height,
36
+ provider,
37
+ provider_url,
38
+ provider_uid
39
+ )
40
+ @url = url
41
+ @thumbnail_url = thumbnail_url
42
+ @title = title
43
+ @width = width
44
+ @height = height
45
+ @provider = provider
46
+ @provider_url = provider_url
47
+ @provider_uid = provider_uid
48
+ end
49
+
50
+ def iframe_embed(width = self.width, height = self.height)
51
+ # rubocop:disable Metrics/LineLength
52
+ if provider == 'youtube'
53
+ %(<iframe width="#{width}" height="#{height}" src="//www.youtube.com/embed/#{provider_uid}?rel=0" frameborder="0" allowfullscreen></iframe>)
54
+ elsif provider == 'vimeo'
55
+ %(<iframe src="//player.vimeo.com/video/#{provider_uid}?title=0&amp;byline=0&amp;portrait=0" width="#{width}" height="#{height}" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>)
56
+ elsif provider == 'facebook'
57
+ %(<iframe src="//www.facebook.com/plugins/video.php?href=#{url}&width=#{width}&show_text=false&height=#{height}" width="#{width}" height="#{height}" style="border:none;overflow:hidden;width:100%;" scrolling="no" frameborder="0" allowTransparency="true" allow="encrypted-media" allowFullScreen="true"></iframe>)
58
+ end
59
+ # rubocop:enable Metrics/LineLength
60
+ end
61
+
62
+ def to_hash(*_args)
63
+ {
64
+ url: url,
65
+ thumbnail_url: thumbnail_url,
66
+ title: title,
67
+ width: width,
68
+ height: height,
69
+ provider: provider,
70
+ provider_url: provider_url,
71
+ provider_uid: provider_uid
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+ require 'forwardable'
3
+ require 'active_support/inflector/transliterate'
4
+ require 'active_support/hash_with_indifferent_access'
5
+ require 'agave/utils/locale_value'
6
+
7
+ Dir[File.dirname(__FILE__) + '/field_type/*.rb'].each do |file|
8
+ require file
9
+ end
10
+
11
+ module Agave
12
+ module Local
13
+ class Item
14
+ extend Forwardable
15
+
16
+ attr_reader :entity
17
+ def_delegators :entity, :id
18
+
19
+ def initialize(entity, items_repo)
20
+ @entity = entity
21
+ @items_repo = items_repo
22
+ end
23
+
24
+ def ==(other)
25
+ other.is_a?(Item) && other.id == id
26
+ end
27
+
28
+ def seo_meta_tags
29
+ Utils::SeoTagsBuilder.new(self, @items_repo.site).meta_tags
30
+ end
31
+
32
+ def singleton?
33
+ item_type.singleton
34
+ end
35
+ alias single_instance? singleton?
36
+
37
+ def item_type
38
+ @item_type ||= entity.item_type
39
+ end
40
+
41
+ def fields
42
+ @fields ||= item_type.fields.sort_by(&:position)
43
+ end
44
+
45
+ def attributes
46
+ fields.each_with_object(
47
+ ActiveSupport::HashWithIndifferentAccess.new
48
+ ) do |field, acc|
49
+ acc[field.api_key.to_sym] = send(field.api_key)
50
+ end
51
+ end
52
+
53
+ def position
54
+ entity.position
55
+ end
56
+
57
+ def parent
58
+ @items_repo.find(entity.parent_id) if item_type.tree && entity.parent_id
59
+ end
60
+
61
+ def children
62
+ @items_repo.children_of(id).sort_by(&:position) if item_type.tree
63
+ end
64
+
65
+ def updated_at
66
+ Time.parse(entity.updated_at).utc
67
+ end
68
+
69
+ def created_at
70
+ # @TODO ?
71
+ # Time.parse(entity.created_at).utc
72
+ end
73
+
74
+ def to_s
75
+ api_key = item_type.api_key
76
+ "#<Item id=#{id} item_type=#{api_key} attributes=#{attributes}>"
77
+ end
78
+ alias inspect to_s
79
+
80
+ def to_hash(max_depth = 3, current_depth = 0)
81
+ return id if current_depth >= max_depth
82
+
83
+ base = {
84
+ id: id,
85
+ item_type: item_type.api_key,
86
+ updated_at: updated_at,
87
+ created_at: created_at
88
+ }
89
+
90
+ base[:position] = position if item_type.sortable
91
+
92
+ if item_type.tree
93
+ base[:position] = position
94
+ base[:children] = children.map do |child|
95
+ child.to_hash(
96
+ max_depth,
97
+ current_depth + 1
98
+ )
99
+ end
100
+ end
101
+
102
+ fields.each_with_object(base) do |field, result|
103
+ value = send(field.api_key)
104
+
105
+ result[field.api_key.to_sym] =
106
+ if value.respond_to?(:to_hash)
107
+ m = value.method(:to_hash)
108
+ if m.arity == 2
109
+ value.to_hash(max_depth, current_depth + 1)
110
+ else
111
+ value.to_hash
112
+ end
113
+ else
114
+ value
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def read_attribute(method, field)
122
+ field_type = field.field_type
123
+ type_klass_name = "::Agave::Local::FieldType::#{field_type.camelize}"
124
+ type_klass = type_klass_name.safe_constantize
125
+
126
+ value = if field.localized
127
+ obj = entity.send(method) || {}
128
+ Utils::LocaleValue.find(obj)
129
+ else
130
+ entity.send(method)
131
+ end
132
+
133
+ if type_klass
134
+ type_klass.parse(value, @items_repo)
135
+ else
136
+ warning = [
137
+ "Warning: unrecognized field of type `#{field_type}`",
138
+ "for item `#{item_type.api_key}` and",
139
+ "field `#{method}`: returning a simple Hash instead.",
140
+ 'Please upgrade to the latest version of the `agave` gem!'
141
+ ]
142
+ puts warning.join(' ')
143
+
144
+ value
145
+ end
146
+ end
147
+
148
+ def method_missing(method, *arguments, &block)
149
+ field = fields.find { |f| f.api_key.to_sym == method }
150
+ if field && arguments.empty?
151
+ read_attribute(method, field)
152
+ else
153
+ super
154
+ end
155
+ rescue NoMethodError => e
156
+ if e.name === method
157
+ message = []
158
+ message << "Undefined method `#{method}`"
159
+ message << "Available fields for a `#{item_type.api_key}` item:"
160
+ message += fields.map do |f|
161
+ "* .#{f.api_key}"
162
+ end
163
+ raise NoMethodError, message.join("\n")
164
+ else
165
+ raise e
166
+ end
167
+ end
168
+
169
+ def respond_to_missing?(method, include_private = false)
170
+ field = fields.find { |f| f.api_key.to_sym == method }
171
+ if field
172
+ true
173
+ else
174
+ super
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/string'
3
+ require 'agave/local/item'
4
+ require 'agave/local/site'
5
+
6
+ module Agave
7
+ module Local
8
+ class ItemsRepo
9
+ attr_reader :entities_repo, :collections_by_type, :item_type_methods
10
+
11
+ def initialize(entities_repo)
12
+ @entities_repo = entities_repo
13
+ @collections_by_type = {}
14
+ @items_by_id = {}
15
+ @items_by_parent_id = {}
16
+ @item_type_methods = {}
17
+
18
+ build_cache!
19
+ end
20
+
21
+ def find(id)
22
+ @items_by_id[id.to_s]
23
+ end
24
+
25
+ def children_of(id)
26
+ @items_by_parent_id.fetch(id.to_s, [])
27
+ end
28
+
29
+ def respond_to_missing?(method, include_private = false)
30
+ if collections_by_type.key?(method)
31
+ true
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def site
38
+ Site.new(
39
+ entities_repo.find_entities_of_type('site').first,
40
+ self
41
+ )
42
+ end
43
+
44
+ def available_locales
45
+ site.locales.map(&:to_sym)
46
+ end
47
+
48
+ def item_types
49
+ entities_repo.find_entities_of_type('item_type')
50
+ end
51
+
52
+ def single_instance_item_types
53
+ item_types.select(&:singleton)
54
+ end
55
+
56
+ def collection_item_types
57
+ item_types.select do |item_type|
58
+ !item_type.singleton
59
+ end
60
+ end
61
+
62
+ def items_of_type(item_type)
63
+ method = item_type_methods[item_type]
64
+
65
+ if item_type.singleton
66
+ Array(@collections_by_type[method])
67
+ else
68
+ @collections_by_type[method]
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def build_cache!
75
+ build_item_type_methods!
76
+ build_collections_by_type!
77
+ build_singletons_by_type!
78
+ end
79
+
80
+ def build_item_type_methods!
81
+ @item_type_methods = {}
82
+
83
+ singleton_keys = single_instance_item_types.map(&:api_key)
84
+ collection_keys = collection_item_types.map(&:api_key)
85
+ .map(&:pluralize)
86
+
87
+ clashing_keys = singleton_keys & collection_keys
88
+
89
+ item_types.each do |item_type|
90
+ pluralized_api_key = item_type.api_key.pluralize
91
+
92
+ method = if item_type.singleton
93
+ item_type.api_key
94
+ else
95
+ pluralized_api_key
96
+ end
97
+
98
+ if clashing_keys.include?(pluralized_api_key)
99
+ suffix = item_type.singleton ? 'instance' : 'collection'
100
+ method = "#{method}_#{suffix}"
101
+ end
102
+
103
+ @item_type_methods[item_type] = method.to_sym
104
+ end
105
+ end
106
+
107
+ def build_collections_by_type!
108
+ item_types.each do |item_type|
109
+ method = item_type_methods[item_type]
110
+ @collections_by_type[method] = if item_type.singleton
111
+ nil
112
+ else
113
+ ItemCollection.new
114
+ end
115
+ end
116
+
117
+ item_entities.each do |item_entity|
118
+ item = Item.new(item_entity, self)
119
+ method = item_type_methods[item_entity.item_type]
120
+
121
+ unless item_entity.item_type.singleton
122
+ @collections_by_type[method].push item
123
+ end
124
+
125
+ @items_by_id[item.id] = item
126
+
127
+ if item_entity.respond_to?(:parent_id) && item_entity.parent_id
128
+ @items_by_parent_id[item_entity.parent_id] ||= []
129
+ @items_by_parent_id[item_entity.parent_id] << item
130
+ end
131
+ end
132
+
133
+ item_types.each do |item_type|
134
+ method = item_type_methods[item_type]
135
+ if !item_type.singleton && item_type.sortable
136
+ @collections_by_type[method].sort_by!(&:position)
137
+ elsif item_type.ordering_field
138
+ @collections_by_type[method].sort_by! do |item|
139
+ item.send(item_type.ordering_field.api_key)
140
+ end
141
+ if item_type.ordering_direction == 'desc'
142
+ @collections_by_type[method].reverse!
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def build_singletons_by_type!
149
+ item_types.each do |item_type|
150
+ method = item_type_methods[item_type]
151
+ next unless item_type.singleton
152
+
153
+ item = if item_type.singleton_item
154
+ @items_by_id[item_type.singleton_item.id]
155
+ end
156
+
157
+ @collections_by_type[method] = item
158
+ end
159
+ end
160
+
161
+ def item_entities
162
+ entities_repo.find_entities_of_type('item')
163
+ end
164
+
165
+ def method_missing(method, *arguments, &block)
166
+ if collections_by_type.key?(method) && arguments.empty?
167
+ collections_by_type[method]
168
+ else
169
+ super
170
+ end
171
+ rescue NoMethodError
172
+ message = []
173
+ message << "Undefined method `#{method}`"
174
+ message << 'Available AgaveCMS collections/items:'
175
+ message += collections_by_type.map do |key, _value|
176
+ "* .#{key}"
177
+ end
178
+ raise NoMethodError, message.join("\n")
179
+ end
180
+
181
+ class ItemCollection < Array
182
+ def each(&block)
183
+ if block && block.arity == 2
184
+ each_with_object({}) do |item, acc|
185
+ acc[item.id] = item
186
+ end.each(&block)
187
+ else
188
+ super(&block)
189
+ end
190
+ end
191
+
192
+ def [](id)
193
+ if id.is_a? String
194
+ find { |item| item.id == id }
195
+ else
196
+ super(id)
197
+ end
198
+ end
199
+
200
+ def keys
201
+ map(&:id)
202
+ end
203
+
204
+ def values
205
+ to_a
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end