cts-mpx-aci 2.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +64 -0
  3. data/Gemfile +30 -0
  4. data/Gemfile.lock +178 -0
  5. data/Guardfile +40 -0
  6. data/LICENSE +201 -0
  7. data/README.md +203 -0
  8. data/Rakefile +6 -0
  9. data/Rules +53 -0
  10. data/bin/console +7 -0
  11. data/bin/setup +8 -0
  12. data/content/CHANGELOG.md +1 -0
  13. data/content/EXAMPLES.md +1 -0
  14. data/content/README.md +1 -0
  15. data/content/assets/bootstrap.min.css +12 -0
  16. data/content/assets/images/cts-logo-wide.svg +121 -0
  17. data/content/assets/images/cts-logo.svg +119 -0
  18. data/content/assets/syntax.css +210 -0
  19. data/content/coverage +1 -0
  20. data/content/doc +1 -0
  21. data/content/specifications.html +1 -0
  22. data/content/stylesheet.css +101 -0
  23. data/cts-mpx-aci.gemspec +23 -0
  24. data/data/stencils/account_record.json +431 -0
  25. data/data/stencils/media_custom_fields.json +37 -0
  26. data/data/stencils/servers.json +31 -0
  27. data/data/stencils/task_templates.json +17 -0
  28. data/data/stencils/test.json +13 -0
  29. data/examples/collect.md +21 -0
  30. data/examples/complete_basic.md +95 -0
  31. data/examples/deploy.md +25 -0
  32. data/examples/image.md +41 -0
  33. data/examples/pre_post_block.md +101 -0
  34. data/layouts/default.html +52 -0
  35. data/lib/cts/mpx/aci/extensions/cts/mpx/entries.rb +29 -0
  36. data/lib/cts/mpx/aci/extensions/cts/mpx/entry.rb +130 -0
  37. data/lib/cts/mpx/aci/extensions/services/data/entry.rb +136 -0
  38. data/lib/cts/mpx/aci/stencil.rb +148 -0
  39. data/lib/cts/mpx/aci/tasks/collect.rb +91 -0
  40. data/lib/cts/mpx/aci/tasks/deploy.rb +117 -0
  41. data/lib/cts/mpx/aci/tasks/image.rb +161 -0
  42. data/lib/cts/mpx/aci/transformations.rb +144 -0
  43. data/lib/cts/mpx/aci/validators.rb +114 -0
  44. data/lib/cts/mpx/aci/version.rb +7 -0
  45. data/lib/cts/mpx/aci.rb +76 -0
  46. data/nanoc.yaml +22 -0
  47. metadata +158 -0
@@ -0,0 +1,29 @@
1
+ module Cts
2
+ module Mpx
3
+ # extensions to the Cts::Mpx::Collection
4
+ class Entries
5
+ # All entries in the collections.
6
+ #
7
+ # @return [hash] of entries with filepath as key and md5 hash as value
8
+ def files
9
+ hash = {}
10
+ entries.map { |e| hash.store "#{e.directory}/#{e.filename}", e.hash }
11
+ hash
12
+ end
13
+
14
+ # transform every entry
15
+ def transform(user)
16
+ each { |entry| entry.transform user }
17
+ nil
18
+ end
19
+
20
+ # untransform every entry
21
+ #
22
+ # @param [String] target_account to transform to
23
+ def untransform(user, target_account)
24
+ each { |entry| entry.untransform(user, target_account) }
25
+ nil
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,130 @@
1
+ module Cts
2
+ module Mpx
3
+ # extensions to the Cts::Mpx::Entry
4
+ class Entry
5
+ # Collection of dependencies required to deploy this entry
6
+ #
7
+ # @return [Array] unordered list of dependencies
8
+ def dependencies
9
+ dependencies = []
10
+ Cts::Mpx::Aci::Transformations.traverse_for(to_h[:entry], :transform) do |_k, v|
11
+ next if v.start_with? "http://access.auth.theplatform.com/data/Account"
12
+ next if v == "urn:cts:aci:target-account"
13
+
14
+ dependencies.push v
15
+ end
16
+ dependencies.uniq
17
+ end
18
+
19
+ # return the difference between two entries
20
+ def diff(other_entry)
21
+ raise ArgumentError, 'an entry must be supplied' unless other_entry.class == self.class
22
+ raise ArgumentError, 'both entries must have the same service' unless service == other_entry.service
23
+ raise ArgumentError, 'both entries must have the same endpoint' unless endpoint == other_entry.endpoint
24
+
25
+ Diffy::Diff.new(to_s, other_entry.to_s).to_s
26
+ end
27
+
28
+ # @return [String] computed filename of the entry.
29
+ def filename
30
+ raise 'Entry must include a guid field' unless fields.collection.map(&:name).include? 'guid'
31
+
32
+ "#{fields['guid']}.json"
33
+ end
34
+
35
+ # @return [String] computed path of the entry.
36
+ def directory
37
+ "#{service}/#{endpoint}"
38
+ end
39
+
40
+ def exists_by?(user, query)
41
+ raise ArgumentError, "user must be signed in" unless user&.token
42
+ raise ArgumentError, "query must be a type of Query" unless query.is_a? Query
43
+
44
+ query.run(user: user)
45
+ return true if query.page.entries&.count&.positive?
46
+
47
+ false
48
+ end
49
+
50
+ # @return [String] complete filepath for an entry
51
+ def filepath
52
+ "#{directory}/#{filename}"
53
+ end
54
+
55
+ # @return [String] MD5 hash, based on formatted JSON
56
+ def hash
57
+ Digest::MD5.hexdigest to_s
58
+ end
59
+
60
+ # true if any reference is found in the entry, false otherwise.
61
+ #
62
+ # @return [Boolean]
63
+ def includes_reference?
64
+ included = false
65
+ Cts::Mpx::Aci::Transformations.traverse_for(to_h[:entry], :transform) do |_k, v|
66
+ included = true
67
+ v
68
+ end
69
+ included
70
+ end
71
+
72
+ # true if any transformed reference is found in the entry, false otherwise.
73
+ #
74
+ # @return [Boolean]
75
+ def includes_transformed_reference?
76
+ ref = false
77
+
78
+ Cts::Mpx::Aci::Transformations.traverse_for(to_h[:entry], :untransform) do |_k, v|
79
+ ref = true
80
+ v
81
+ end
82
+ ref
83
+ end
84
+
85
+ # transform all references in this entry
86
+ # @raise [RuntimeError] if guid field is not included
87
+ # @raise [RuntimeError] if ownerId field is not included
88
+ # @raise [RuntimeError] if user is not set
89
+ def transform(user)
90
+ raise_entry_exceptions!(user)
91
+ hash = to_h[:entry]
92
+ output = Cts::Mpx::Aci::Transformations.traverse_for(hash, :transform) do |_k, v|
93
+ if Cts::Mpx::Aci::Validators.field_reference? v
94
+ Cts::Mpx::Aci::Transformations.transform_field_reference field_reference: v, user: user
95
+ else
96
+ Cts::Mpx::Aci::Transformations.transform_reference reference: v, user: user, original_account: fields['ownerId']
97
+ end
98
+ end
99
+ @fields = Fields.create_from_data(data: output, xmlns: to_h[:namespace])
100
+ self
101
+ end
102
+
103
+ # untransform all transformed references in this entry
104
+ # @raise [RuntimeError] if guid field is not included
105
+ # @raise [RuntimeError] if ownerId field is not included
106
+ # @raise [RuntimeError] if user is not set
107
+ def untransform(user, target_account)
108
+ raise_entry_exceptions!(user)
109
+ hash = to_h[:entry]
110
+ output = Cts::Mpx::Aci::Transformations.traverse_for(hash, :untransform) do |_k, v|
111
+ if Cts::Mpx::Aci::Validators.transformed_field_reference? v
112
+ Cts::Mpx::Aci::Transformations.untransform_field_reference transformed_reference: v, user: user
113
+ else
114
+ Cts::Mpx::Aci::Transformations.untransform_reference transformed_reference: v, user: user, target_account: target_account
115
+ end
116
+ end
117
+ @fields = Fields.create_from_data(data: output, xmlns: to_h[:namespace])
118
+ self
119
+ end
120
+
121
+ private
122
+
123
+ def raise_entry_exceptions!(user)
124
+ raise 'Entry must include an guid field' unless fields.to_h.key?('guid')
125
+ raise 'Entry must include an ownerId field' unless fields.to_h.key?('ownerId')
126
+ raise 'Entry must have user set' unless user
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,136 @@
1
+ module Theplatform
2
+ module Services
3
+ module Data
4
+ # extensions to the Theplatform::Services::Data::Entry
5
+ class Entry
6
+ # Collection of dependencies required to deploy this entry
7
+ #
8
+ # @return [Array] unordered list of dependencies
9
+ def dependencies
10
+ dependencies = []
11
+ Cts::Mpx::Aci::Transformations.traverse_for(to_hash['entry'], :transform) do |_k, v|
12
+ next if v.start_with? "http://access.auth.theplatform.com/data/Account"
13
+ next if v == "urn:cts:aci:target-account"
14
+ dependencies.push v
15
+ v
16
+ end
17
+ dependencies.uniq
18
+ end
19
+
20
+ # return the difference between two entri
21
+
22
+ # true if any reference is found in the entry, false otherwise.
23
+ #
24
+ # @return [Boolean]
25
+ def includes_reference?
26
+ output = false
27
+ Cts::Mpx::Aci::Transformations.traverse_for(to_hash['entry'], :transform) do |_k, v|
28
+ output = true
29
+ v
30
+ end
31
+ output
32
+ end
33
+
34
+ # true if any transformed reference is found in the entry, false otherwise.
35
+ #
36
+ # @return [Boolean]
37
+ def includes_transformed_reference?
38
+ ref = false
39
+ Cts::Mpx::Aci::Transformations.traverse_for(to_hash['entry'], :untransform) do |_k, v|
40
+ ref = true
41
+ v
42
+ end
43
+ ref
44
+ end
45
+
46
+ # @return [String] computed filename of the entry.
47
+ def filename
48
+ raise 'Entry must include an guid field' unless fields.include? 'guid'
49
+ "#{entry['guid']}.json"
50
+ end
51
+
52
+ # @return [String] computed path of the entry.
53
+ def directory
54
+ "#{service}/#{endpoint}"
55
+ end
56
+
57
+ # @return [String] complete filepath for an entry
58
+ def filepath
59
+ "#{directory}/#{filename}"
60
+ end
61
+
62
+ # @return [String] MD5 hash, based on formatted JSON
63
+ def hash
64
+ Digest::MD5.hexdigest to_s
65
+ end
66
+
67
+ # return the difference between two entries
68
+ def diff(other_entry)
69
+ raise ArgumentError, 'an entry must be supplied' unless other_entry.class == self.class
70
+ raise ArgumentError, 'both entries must have the same service' unless service == other_entry.service
71
+ raise ArgumentError, 'both entries must have the same endpoint' unless endpoint == other_entry.endpoint
72
+ Diffy::Diff.new(to_s, other_entry.to_s).to_s
73
+ end
74
+
75
+ # transform all references in this entry
76
+ # @raise [RuntimeError] if guid field is not included
77
+ # @raise [RuntimeError] if ownerId field is not included
78
+ # @raise [RuntimeError] if user is not set
79
+ def transform
80
+ raise_entry_exceptions!
81
+ hash = to_hash['entry']
82
+ output = Cts::Mpx::Aci::Transformations.traverse_for(hash, :transform) do |_k, v|
83
+ if Cts::Mpx::Aci::Validators.field_reference? v
84
+ Cts::Mpx::Aci::Transformations.transform_field_reference field_reference: v, user: user
85
+ else
86
+ Cts::Mpx::Aci::Transformations.transform_reference reference: v, user: user, original_account: ownerId
87
+ end
88
+ end
89
+ load_from_hash "$xmlns" => namespace, 'entry' => { "id" => id }.merge(output)
90
+ end
91
+
92
+ # untransform all transformed references in this entry
93
+ # @raise [RuntimeError] if guid field is not included
94
+ # @raise [RuntimeError] if ownerId field is not included
95
+ # @raise [RuntimeError] if user is not set
96
+ def untransform(target_account)
97
+ raise_entry_exceptions!
98
+ hash = to_hash['entry']
99
+ output = Cts::Mpx::Aci::Transformations.traverse_for(hash, :untransform) do |_k, v|
100
+ if Cts::Mpx::Aci::Validators.transformed_field_reference? v
101
+ Cts::Mpx::Aci::Transformations.untransform_field_reference transformed_reference: v, user: user
102
+ else
103
+ Cts::Mpx::Aci::Transformations.untransform_reference transformed_reference: v, user: user, target_account: target_account
104
+ end
105
+ end
106
+ load_from_hash "$xmlns" => namespace, 'entry' => { "id" => id }.merge(output)
107
+ end
108
+
109
+ def endpoint
110
+ @endpoint.tr '/', ''
111
+ end
112
+
113
+ # :nocov:
114
+ # addresses a bug in the SDK. in the load from hash, the result['entries']. Adding '.first' is the fix so only an entry is returned, not an array.
115
+ def save_to_service
116
+ if existing_entry?
117
+ service_module.send "put_#{endpoint.split(/(?=[A-Z])/).join('_').downcase}", user, [to_hash(include_read_only: false)["entry"]], namespace: namespace
118
+ else
119
+ result = service_module.send "post_#{endpoint.split(/(?=[A-Z])/).join('_').downcase}", user, [to_hash(include_read_only: false)["entry"]], namespace: namespace
120
+ load_from_hash("entry" => result['entries'].first) if result
121
+ end
122
+ true
123
+ end
124
+ # :nocov:
125
+
126
+ private
127
+
128
+ def raise_entry_exceptions!
129
+ raise 'Entry must include an guid field' unless fields.include? 'guid'
130
+ raise 'Entry must include an ownerId field' unless fields.include? 'ownerId'
131
+ raise 'Entry must have user set' unless user
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,148 @@
1
+ module Cts
2
+ module Mpx
3
+ module Aci
4
+ # Container for a selection of queries to run on an account.
5
+ # @!attribute [rw] name
6
+ # @return [String] Name of the stencil
7
+ # @!attribute [rw] original_url
8
+ # @return [String] If applicable, the original location of the asset.
9
+ # @!attribute [rw] schema
10
+ # @return [String] Schema of this stencil
11
+ # @!attribute [rw] queries
12
+ # @return [String] Queries to perform in the collect object
13
+ class Stencil
14
+ include Creatable
15
+
16
+ attribute name: 'name', kind_of: String
17
+ attribute name: 'original_url', kind_of: String
18
+ attribute name: 'schema', kind_of: String
19
+ attribute name: 'queries', kind_of: Array
20
+
21
+ # find a stencil in the loaded stencils by name. when used with no parameter will show all available stencils.
22
+ #
23
+ # @param key [String] name of the stencil
24
+ # @return [Stencil] referenced stencil
25
+ def self.[](key = nil)
26
+ Stencil.class_variable_set(:@@available_stencils, {}) unless Stencil.class_variable_defined? :@@available_stencils
27
+ return @@available_stencils[key] if key
28
+
29
+ @@available_stencils
30
+ end
31
+
32
+ # Smart load a stencil. This will attemp to parse it as json, then load it as a url, and finally try to load a file.
33
+ #
34
+ # @param string [String] string to attempt to smart load.
35
+ # @return [Stencil] loaded stencil
36
+ def self.load(string)
37
+ begin
38
+ data = load_string Stencil.parse_json(string)
39
+ rescue ArgumentError
40
+ return load_url(string) if string.start_with? 'http'
41
+
42
+ return load_file string
43
+ end
44
+
45
+ load_string(string) if data
46
+ end
47
+
48
+ # Attempt to open a local file for reading, loading in the JSON if it is able.
49
+ #
50
+ # @param file [String] file of the file to load.
51
+ # @raise [RuntimeError] If file could not be found
52
+ # @raise [RuntimeError] If file is empty.
53
+ # @return [Stencil] loaded stencil
54
+ def self.load_file(file)
55
+ raise "Could not find file #{file}." unless File.exist? file
56
+
57
+ content = File.read(file)
58
+ raise RuntimeError if content.empty?
59
+
60
+ stencil = load_string(content)
61
+ stencil.original_url = file
62
+ stencil
63
+ end
64
+
65
+ # Attempt to open a local file for reading, loading in the JSON if it is able.
66
+ #
67
+ # @param string [String] string to be parsed and loaded.
68
+ # @raise [ArgumentError] if the parsed queries is not an array
69
+ # @return [Stencil] loaded stencil
70
+ def self.load_string(string)
71
+ data = Stencil.parse_json(string)
72
+
73
+ stencil = Stencil.new
74
+ stencil.name = data['name']
75
+ raise ArgumentError, "queries is not a kind of Array" unless data['queries'].is_a? Array
76
+
77
+ stencil.queries = data['queries']
78
+
79
+ @@available_stencils[stencil.name] = stencil
80
+ stencil
81
+ end
82
+
83
+ # Attempt to open a local file for reading, loading in the JSON if it is able.
84
+ #
85
+ # @param url [String] url to be fetched and passed to load_string
86
+ # @raise [ArgumentError] if the url is not valid
87
+ # @return [Stencil] loaded stencil
88
+ def self.load_url(url)
89
+ begin
90
+ URI.parse url
91
+ rescue URI::InvalidURIError
92
+ raise ArgumentError, "#{url} is not a url"
93
+ end
94
+
95
+ load_string(Excon.get(url).body)
96
+ end
97
+
98
+ # Attempt to open a local file for reading, loading in the JSON if it is able.
99
+ #
100
+ # @param string [String] string is parsed and returned as a hash
101
+ # @raise [ArgumentError] if the arugment is not a type of a string
102
+ # @raise [ArgumentError] if the string could not be parsed
103
+ # @return [Hash] hash of the data from the json
104
+ def self.parse_json(string)
105
+ raise ArgumentError, "#{string.class} is not a kind of String" unless string&.is_a?(String)
106
+
107
+ begin
108
+ data = Oj.load(string)
109
+ rescue Oj::ParseError
110
+ raise ArgumentError, "could not be parsed"
111
+ end
112
+
113
+ data
114
+ end
115
+
116
+ def initialize
117
+ Stencil.class_variable_set(:@@available_stencils, {}) unless Stencil.class_variable_defined? :@@available_stencils
118
+ @name = ""
119
+ @original_url = ""
120
+ @queries = []
121
+ @schema = 1
122
+ end
123
+
124
+ # Attempt to open a local file for reading, loading in the JSON if it is able.
125
+ #
126
+ # @param account_id [String] account_id account_id to collect from
127
+ # @param user [String] user to use to make the collection.
128
+ # @raise [ArgumentError] if the arugment is not a type of a string
129
+ # @raise [ArgumentError] if no queries are provided
130
+ # @return [Collect] a collect object created from the json and parameters passed in.
131
+ def to_collect(account_id: nil, user: nil)
132
+ raise ArgumentError, 'queries must contain entries' unless queries.any?
133
+
134
+ collect = Tasks::Collect.new
135
+ collect.account_id = account_id if account_id
136
+ collect.user = user if user
137
+
138
+ queries.each do |q|
139
+ query = Query.create service: q[:service], endpoint: q[:endpoint]
140
+ collect.queries.push query
141
+ end
142
+
143
+ collect
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,91 @@
1
+ module Cts
2
+ module Mpx
3
+ module Aci
4
+ module Tasks
5
+ # Collect class for gathering data from MPX.
6
+ class Collect
7
+ include Creatable
8
+
9
+ attribute name: 'account_id', kind_of: String
10
+ attribute name: 'user', kind_of: User
11
+ attribute name: 'queries', kind_of: Array
12
+ attribute name: 'entries', kind_of: Entries
13
+
14
+ # Executes the array of queries one by one. Collecting the result into the collections attribute.
15
+ # @raise [RuntimeError] when queries is not set correctly
16
+ # @raise [ArgumentError] when the user attribute is not set
17
+ # @raise [ArgumentError] when the account_id attribute is not set
18
+ # @raise [ArgumentError] when a query does not have service set
19
+ # @raise [ArgumentError] when a query does not have endpoint set
20
+ def collect
21
+ raise "empty queries array" if @queries&.empty?
22
+ raise 'must set the queries attribute.' unless queries
23
+
24
+ @entries = Entries.new
25
+
26
+ queries.each do |config|
27
+ query = run_query(config)
28
+
29
+ if query&.entries.count.positive?
30
+ log_collected_zero_entries config
31
+ next
32
+ end
33
+
34
+ @entries += query.page.to_mpx_entries
35
+ @entries.each do |entry|
36
+ # if we collect the read only fields, it just messes everything up.
37
+ # best to just not collect them.
38
+ service_read_only_fields.each { |field| entry.fields.remove field }
39
+
40
+ log_collected entry
41
+ end
42
+ end
43
+ end
44
+
45
+ def initialize
46
+ @account_id = ""
47
+ @user = nil
48
+ @entries = Entries.new
49
+ @queries = []
50
+ end
51
+
52
+ def queries=(new_queries)
53
+ raise ArgumentError unless new_queries.is_a? Array
54
+ raise ArgumentError if new_queries.map { |e| e.is_a? Hash }.include? false
55
+
56
+ @queries = new_queries
57
+ end
58
+
59
+ private
60
+
61
+ def service_read_only_fields
62
+ ["updated", "added", "addedByUserId", "updatedByUserId", "version"]
63
+ end
64
+
65
+ def logger
66
+ Aci.logger
67
+ end
68
+
69
+ def log_collected(entry)
70
+ logger.info "collected: id: #{entry.fields['id']} guid: #{entry.fields['guid']}"
71
+ end
72
+
73
+ def log_collected_zero_entries(config)
74
+ logger.info "collected zero results for #{config['service']}/#{config['endpoint']}"
75
+ end
76
+
77
+ def run_query(config)
78
+ raise ArgumentError, 'must set the user attribute.' unless user
79
+ raise ArgumentError, 'must set the account_id attribute.' unless account_id
80
+
81
+ raise ArgumentError, "#{config} does not have service set" if config["service"].empty?
82
+ raise ArgumentError, "#{config} does not have endpoint set" if config["endpoint"].empty?
83
+
84
+ query = Query.create config
85
+ query.run user: user
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end