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,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