agave-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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