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,78 @@
1
+ # frozen_string_literal: true
2
+ module Agave
3
+ module Local
4
+ class JsonApiEntity
5
+ attr_reader :payload, :data_source
6
+
7
+ def initialize(payload, data_source)
8
+ @payload = payload
9
+ @data_source = data_source
10
+ end
11
+
12
+ def id
13
+ @payload[:id]
14
+ end
15
+
16
+ def type
17
+ @payload[:type]
18
+ end
19
+
20
+ def ==(other)
21
+ if other.is_a? JsonApiEntity
22
+ id == other.id && type == other.type
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def to_s
29
+ "#<JsonApiEntity id=#{id} type=#{type} payload=#{payload}>"
30
+ end
31
+ alias inspect to_s
32
+
33
+ def [](key)
34
+ attributes[key]
35
+ end
36
+
37
+ def respond_to_missing?(method, include_private = false)
38
+ if attributes.key?(method) || relationships.key?(method)
39
+ true
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def attributes
48
+ @payload.fetch(:attributes, {})
49
+ end
50
+
51
+ def relationships
52
+ @payload.fetch(:relationships, {})
53
+ end
54
+
55
+ def dereference_linkage(linkage)
56
+ if linkage.is_a? Array
57
+ linkage.map do |item|
58
+ data_source.find_entity(item[:type], item[:id])
59
+ end
60
+ elsif linkage.is_a? Hash
61
+ data_source.find_entity(linkage[:type], linkage[:id])
62
+ end
63
+ end
64
+
65
+ def method_missing(method, *arguments, &block)
66
+ return super unless arguments.empty?
67
+
68
+ if attributes.key?(method)
69
+ attributes[method]
70
+ elsif relationships.key?(method)
71
+ dereference_linkage(relationships[method][:data])
72
+ else
73
+ super
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'agave/local/entities_repo'
3
+ require 'agave/local/items_repo'
4
+
5
+ module Agave
6
+ module Local
7
+ class Loader
8
+ attr_reader :client
9
+ attr_reader :entities_repo
10
+ attr_reader :items_repo
11
+ attr_reader :preview_mode
12
+
13
+ def initialize(client, preview_mode = false)
14
+ @client = client
15
+ @preview_mode = preview_mode
16
+ @entities_repo = EntitiesRepo.new
17
+ @items_repo = ItemsRepo.new(@entities_repo)
18
+ end
19
+
20
+ def load
21
+ threads = [
22
+ Thread.new { Thread.current[:output] = site },
23
+ Thread.new { Thread.current[:output] = all_items },
24
+ Thread.new { Thread.current[:output] = all_uploads }
25
+ ]
26
+
27
+ results = threads.map do |t|
28
+ t.join
29
+ t[:output]
30
+ end
31
+
32
+ @entities_repo = EntitiesRepo.new(*results)
33
+ @items_repo = ItemsRepo.new(@entities_repo)
34
+ end
35
+
36
+ private
37
+
38
+ def site
39
+ include = ['item_types', 'item_types.fields']
40
+ client.request(:get, '/api/site', include: include)
41
+ end
42
+
43
+ def all_items
44
+ client.items.all(
45
+ { version: preview_mode ? 'latest' : 'published' },
46
+ deserialize_response: false,
47
+ all_pages: true
48
+ )
49
+ end
50
+
51
+ def all_uploads
52
+ client.uploads.all(
53
+ { "filter[type]" => "used" },
54
+ deserialize_response: false,
55
+ all_pages: true
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,73 @@
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
+ module Agave
8
+ module Local
9
+ class Site
10
+ extend Forwardable
11
+
12
+ attr_reader :entity
13
+ def_delegators :entity, :id, :name, :locales, :domain,
14
+ :no_index, :production_frontend_url
15
+
16
+ def initialize(entity, items_repo)
17
+ @entity = entity
18
+ @items_repo = items_repo
19
+ end
20
+
21
+ def global_seo
22
+ read_attribute(:global_seo, FieldType::GlobalSeo, locales.size > 1)
23
+ end
24
+
25
+ def theme
26
+ read_attribute(:theme, FieldType::Theme, false)
27
+ end
28
+
29
+ def favicon
30
+ read_attribute(:favicon, FieldType::File, false)
31
+ end
32
+
33
+ def to_s
34
+ "#<Site id=#{id} site_name=#{name}>"
35
+ end
36
+ alias inspect to_s
37
+
38
+ def favicon_meta_tags(theme_color = nil)
39
+ Utils::FaviconTagsBuilder.new(self, theme_color).meta_tags
40
+ end
41
+
42
+ def to_hash
43
+ attributes = [
44
+ :id, :name, :locales, :theme, :domain,
45
+ :no_index, :global_seo, :favicon, :production_frontend_url
46
+ ]
47
+
48
+ attributes.each_with_object({}) do |attribute, result|
49
+ value = send(attribute)
50
+ result[attribute] = if value.respond_to?(:to_hash)
51
+ value.to_hash
52
+ else
53
+ value
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def read_attribute(method, type_klass, localized)
61
+ value = if localized
62
+ obj = entity.send(method) || {}
63
+
64
+ Utils::LocaleValue.find(obj)
65
+ else
66
+ entity.send(method)
67
+ end
68
+
69
+ type_klass.parse(value, @items_repo)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ module Agave
2
+ class Paginator
3
+ def initialize(client, base_endpoint, filters)
4
+ @client, @base_endpoint, @filters = client, base_endpoint, filters
5
+ end
6
+
7
+ def response
8
+ items_per_page = 100
9
+
10
+ base_response = @client.request(
11
+ :get, @base_endpoint, @filters.dup.merge('page[limit]' => items_per_page)
12
+ )
13
+
14
+ extra_pages = (
15
+ base_response[:meta][:total_count] / items_per_page.to_f
16
+ ).ceil - 1
17
+
18
+ extra_pages.times do |page|
19
+ base_response[:data] += @client.request(
20
+ :get,
21
+ @base_endpoint,
22
+ @filters.dup.merge(
23
+ 'page[offset]' => items_per_page * (page + 1),
24
+ 'page[limit]' => items_per_page
25
+ )
26
+ )[:data]
27
+ end
28
+
29
+ base_response
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ require 'agave/json_api_serializer'
3
+ require 'agave/json_api_deserializer'
4
+ require 'agave/paginator'
5
+
6
+ module Agave
7
+ class Repo
8
+ attr_reader :client, :type, :schema
9
+
10
+ IDENTITY_REGEXP = /\{\(.*?definitions%2F(.*?)%2Fdefinitions%2Fidentity\)}/
11
+
12
+ METHOD_NAMES = {
13
+ "instances" => :all,
14
+ "self" => :find,
15
+ }
16
+
17
+ def initialize(client, type, schema)
18
+ @client = client
19
+ @type = type
20
+ @schema = schema
21
+
22
+ schema.links.each do |link|
23
+ method_name = METHOD_NAMES.fetch(link.rel, link.rel)
24
+
25
+ self.define_singleton_method(method_name) do |*args|
26
+ min_arguments_count = [
27
+ link.href.scan(IDENTITY_REGEXP).size,
28
+ link.schema && link.method != :get ? 1 : 0
29
+ ].reduce(0, :+)
30
+
31
+ args.size >= min_arguments_count or
32
+ raise ArgumentError.new(
33
+ "wrong number of arguments (given #{args.size}, expected #{min_arguments_count})"
34
+ )
35
+
36
+ placeholders = []
37
+
38
+ url = link["href"].gsub(IDENTITY_REGEXP) do |_stuff|
39
+ placeholder = args.shift.to_s
40
+ placeholders << placeholder
41
+ placeholder
42
+ end
43
+
44
+ response = if %i(post put).include?(link.method)
45
+ body = if link.schema
46
+ unserialized_body = args.shift
47
+
48
+ JsonApiSerializer.new(type, link).serialize(
49
+ unserialized_body,
50
+ link.method == :post ? nil : placeholders.last
51
+ )
52
+ else
53
+ {}
54
+ end
55
+
56
+ client.request(link.method, url, body)
57
+
58
+ elsif link.method == :delete
59
+ client.request(:delete, url)
60
+
61
+ elsif link.method == :get
62
+ query_string = args.shift
63
+
64
+ all_pages = (args[0] || {}).
65
+ symbolize_keys.
66
+ fetch(:all_pages, false)
67
+
68
+ is_paginated_endpoint = link.schema &&
69
+ link.schema.properties.has_key?("page[limit]")
70
+
71
+ if is_paginated_endpoint && all_pages
72
+ Paginator.new(client, url, query_string).response
73
+ else
74
+ client.request(:get, url, query_string)
75
+ end
76
+ end
77
+
78
+ options = if args.any?
79
+ args.shift.symbolize_keys
80
+ else
81
+ {}
82
+ end
83
+
84
+ if options.fetch(:deserialize_response, true)
85
+ JsonApiDeserializer.new.deserialize(response)
86
+ else
87
+ response
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ require 'agave/api_client'
3
+ require 'agave/upload/file'
4
+ require 'agave/upload/image'
5
+
6
+ module Agave
7
+ module Site
8
+ class Client
9
+ include ApiClient
10
+
11
+ json_schema 'site-api'
12
+
13
+ def upload_file(path_or_url)
14
+ file = Upload::File.new(self, path_or_url)
15
+ file.upload
16
+ end
17
+
18
+ def upload_image(path_or_url)
19
+ file = Upload::Image.new(self, path_or_url)
20
+ file.upload
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+ require 'downloadr'
3
+ require 'tempfile'
4
+ require 'addressable'
5
+ require 'net/http'
6
+ require 'fastimage'
7
+
8
+ module Agave
9
+ module Upload
10
+ class File
11
+ IMAGE_FORMATS = %w(png jpg jpeg gif)
12
+
13
+ attr_reader :client, :source
14
+
15
+ def initialize(client, source)
16
+ @client = client
17
+ @source = source
18
+ end
19
+
20
+ def file
21
+ @file ||= if http_source?
22
+ uri = Addressable::URI.parse(source)
23
+ ext = ::File.extname(uri.path)
24
+ Tempfile.new(['file', ext]).tap do |file|
25
+ Downloadr::HTTP.new(source, file).download
26
+ end
27
+ else
28
+ ::File.new(::File.expand_path(source))
29
+ end
30
+ end
31
+
32
+ def http_source?
33
+ uri = Addressable::URI.parse(source)
34
+ uri.scheme == 'http' || uri.scheme == 'https'
35
+ rescue Addressable::URI::InvalidURIError
36
+ false
37
+ end
38
+
39
+ def filename
40
+ if http_source?
41
+ ::File.basename(source)
42
+ else
43
+ ::File.basename(file.path)
44
+ end
45
+ end
46
+
47
+ def upload
48
+ upload_request = client.upload_request.create(filename: filename)
49
+ uri = URI.parse(upload_request[:url])
50
+
51
+ request = Net::HTTP::Put.new(
52
+ uri,
53
+ 'x-amz-acl' => 'public-read'
54
+ )
55
+ request.body = file.read
56
+
57
+ http = Net::HTTP.new(uri.host, uri.port)
58
+ http.use_ssl = true
59
+
60
+ http.request(request)
61
+
62
+ uploads = client.uploads.create(
63
+ format_resource(upload_request)
64
+ )
65
+
66
+ uploads["id"]
67
+ end
68
+
69
+ def format_resource(upload_request)
70
+ extension = ::File.extname(::File.basename(file.path)).delete('.')
71
+
72
+ base_format = {
73
+ path: upload_request[:id],
74
+ size: ::File.size(file.path),
75
+ format: extension
76
+ }
77
+
78
+ if IMAGE_FORMATS.include?(extension)
79
+ width, height = FastImage.size(file.path)
80
+
81
+ base_format.merge(
82
+ width: width,
83
+ height: height,
84
+ format: FastImage.type(file.path).to_s
85
+ )
86
+ else
87
+ base_format
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end