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,117 @@
1
+ module Cts
2
+ module Mpx
3
+ module Aci
4
+ module Tasks
5
+ # Responsible for deploying an image to an account.
6
+ # @!attribute [rw] user
7
+ # @return [User] user to make data service calls with
8
+ # @!attribute [rw] account
9
+ # @return [String] relative account or nil when untransformed
10
+ # @!attribute [rw] image
11
+ # @return [Cts::Mpx::Aci::Tasks::Image]Image containing entries to deploy
12
+ class Deploy
13
+ include Creatable
14
+
15
+ attribute name: 'account', kind_of: String
16
+ attribute name: 'image', kind_of: Tasks::Image
17
+ attribute name: 'user', kind_of: User
18
+ attribute name: 'pre_block', kind_of: Proc
19
+ attribute name: 'post_block', kind_of: Proc
20
+
21
+ # Any dependencies the image may contain
22
+ # @return [Hash] dependency hash keyed by entry
23
+ def dependencies
24
+ hash = {}
25
+ image.entries.each do |e|
26
+ deps = e.dependencies
27
+ hash.store e.id, deps if deps.any?
28
+ end
29
+ hash
30
+ end
31
+
32
+ # rubocop:disable Metrics/AbcSize
33
+ # reason: these classes is as thin as it can get. not splitting it up to satisify rubocop.
34
+ # deploy a transformed image to an account
35
+ # @param [String] target_account to deploy to
36
+ # @raise [RuntimeError] when image is not deployable
37
+ def deploy(target_account, *args)
38
+ raise "not a deployable image" unless Validators.image_deployable? image
39
+ raise "not a deployable image" unless deploy_order
40
+
41
+ deploy_order.each do |ref|
42
+ entry = image.entries.find { |e| e.id == ref }
43
+ entry = block_update_entry entry, *args, &pre_block if pre_block
44
+
45
+ query = Query.create service: entry.service, endpoint: entry.endpoint, fields: 'id,guid'
46
+ query.query['byOwnerId'] = target_account
47
+
48
+ if entry.id.include? 'Field/'
49
+ query.query['byQualifiedFieldName'] = "#{entry.fields['namespace']}$#{entry.fields['fieldName']}"
50
+ else
51
+ query.query['byGuid'] = entry.fields['guid']
52
+ end
53
+
54
+ if entry.exists_by? user, query
55
+ method = 'PUT'
56
+ response = query.run(user: user)
57
+ entry.id = response.page.entries.first["id"]
58
+ else
59
+ entry.id = nil
60
+ entry.service = query.service
61
+ entry.endpoint = query.endpoint
62
+ method = 'POST'
63
+ end
64
+
65
+ entry.fields['ownerId'] = target_account
66
+ entry.save user: user
67
+ block_update_entry entry, *args, &post_block if post_block
68
+
69
+ logger.info "deployed #{entry.fields['guid']} to #{target_account} as #{entry.id || 'new_id'} with #{user.username} via a #{method} call"
70
+ end
71
+
72
+ true
73
+ end
74
+
75
+ # @return [Array] order to deploy objects in
76
+ # @return [nil] if image state is transformed.
77
+ # @return [nil] if a deploy order could not be generated.
78
+ def deploy_order
79
+ return nil unless image.state == :untransformed
80
+
81
+ hash = dependencies
82
+ list = image.entries.map(&:id) - dependencies.keys
83
+
84
+ 100.times do |_i|
85
+ break unless hash.any?
86
+
87
+ hash.delete_if { |k, _v| list.include? k }
88
+
89
+ new_hash = hash.select { |_k, v| (v - list).empty? }
90
+ list += new_hash.keys if new_hash.any?
91
+ end
92
+
93
+ return list.uniq if image.entries.map(&:id).count == list.count
94
+
95
+ nil
96
+ end
97
+ # rubocop:enable Metrics/AbcSize
98
+
99
+ def block_update_entry(entry, *args, &block)
100
+ raise ArgumentError, 'block must be provided' unless block
101
+ raise ArgumentError, 'argument must be an entry' unless entry.is_a? Entry
102
+
103
+ e = entry.dup
104
+ block.yield e, args
105
+ e
106
+ end
107
+
108
+ private
109
+
110
+ def logger
111
+ Aci.logger
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,161 @@
1
+ module Cts
2
+ module Mpx
3
+ module Aci
4
+ module Tasks
5
+ # Image class for gathering a set of service data as a single collection.
6
+ #
7
+ # @!attribute [rw] entries
8
+ # @return [Hash] set of entries to generate the image from
9
+ # @!attribute [rw] schema
10
+ # @return [String] schema of the image
11
+ # @!attribute [rw] user
12
+ # @return [User] user to make data service calls with
13
+ # @!attribute [r] account_id
14
+ # @return [String] relative account_id or nil when untransformed
15
+ # @!attribute [r] date_taken
16
+ # @return [DateTime] Date the image was instantiated
17
+ # @!attribute [r] state
18
+ # @return [String] :transformed or :untransformed
19
+ class Image
20
+ include Creatable
21
+
22
+ attribute name: 'entries', kind_of: Entries
23
+ attribute name: 'schema', kind_of: Integer
24
+ attribute name: 'user', kind_of: User
25
+ attribute name: 'account_id', kind_of: String
26
+ attribute name: 'date_taken', kind_of: Time
27
+ attribute name: 'state', kind_of: String
28
+
29
+ # load an image from directory
30
+ #
31
+ # @param user [String] user to set the image and entries to
32
+ # @return [Cts::Mpx::aciTasks::Image] image with entries loaded and info set.
33
+ def self.load_from_directory(directory, user = nil)
34
+ i = new
35
+ i.load_from_directory directory, user
36
+ i
37
+ end
38
+
39
+ # All entries in the entries, including the md5 hash of the data.
40
+ #
41
+ # @return [Hash] filepath is key, value is md5
42
+ def files
43
+ entries.map(&:filepath)
44
+ end
45
+
46
+ # Generate information report for the image.
47
+ #
48
+ # @return [Hash] contains the information about the image.
49
+ def info
50
+ {
51
+ account_id: @account_id,
52
+ date_taken: @date_taken.iso8601,
53
+ username: @user.nil? ? "" : @user.username,
54
+ schema: @schema,
55
+ state: @state,
56
+ files: files
57
+ }
58
+ end
59
+
60
+ def initialize
61
+ @account_id = ""
62
+ @entries = Entries.new
63
+ @date_taken = Time.now
64
+ @files = {}
65
+ @state = :untransformed
66
+ @schema = 1
67
+ @user = nil
68
+ end
69
+
70
+ # save an image to a directory
71
+ #
72
+ # @param directory [String] the name of the directory to save to
73
+ def save_to_directory(directory)
74
+ entries.each do |entry|
75
+ FileUtils.mkdir_p "#{directory}/#{entry.directory}"
76
+ File.write "#{directory}/#{entry.filepath}", entry.to_s
77
+ end
78
+
79
+ File.write "#{directory}/info.json", Oj.dump(info, indent: 2)
80
+ true
81
+ end
82
+
83
+ # load an image from directory
84
+ #
85
+ # @param user [String] user to set the image and entries to
86
+ # @return [Cts::Mpx::Aci::Tasks::Image] image with entries loaded and info set.
87
+ def load_from_directory(directory, user = nil)
88
+ raise "#{directory} does not contain a valid image." unless Validators.image_directory? directory
89
+
90
+ info = load_json_or_error "#{directory}/info.json"
91
+
92
+ ["date_taken", "account_id", "schema", "username", "state"].each do |param|
93
+ instance_variable_set "@#{param}", info[param]
94
+ end
95
+
96
+ entries = Entries.new
97
+
98
+ info["files"].each do |file|
99
+ begin
100
+ h = load_json_or_error("#{directory}/#{file}")
101
+ rescue Oj::ParseError
102
+ raise "#{directory}/#{file} is readable, but not parsable. Please run the json through a linter."
103
+ end
104
+
105
+ entries.add Entry.create(fields: Fields.create_from_data(data: h[:entry], xmlns: h[:xmlns]))
106
+ end
107
+
108
+ @user = user
109
+
110
+ true
111
+ end
112
+
113
+ # merge two images together
114
+ #
115
+ # @param other_image [Image] Image to merge from
116
+ # @return [Cts::Mpx::Aci::Tasks::Image] new image containg merged results.
117
+ def merge(other_image)
118
+ raise 'an image class must be supplied' unless other_image.is_a? Image
119
+ raise 'cannot merge if the user is different' unless other_image.user == user
120
+ raise 'cannot merge if the account_id is different' unless other_image.account_id == account_id
121
+ raise 'cannot merge if the state is different' unless other_image.state == state
122
+
123
+ new_image = Image.new
124
+ new_image.user = @user
125
+ new_image.entries = entries + other_image.entries
126
+ new_image
127
+ end
128
+
129
+ # transform an image to an abstract state
130
+ def transform
131
+ entries.each { |entry| entry.transform user }
132
+ @state = :transformed
133
+ @account_id = nil
134
+ true
135
+ end
136
+
137
+ # untransform an image from an abstract state
138
+ # @param target_account [String] account_id to transform to
139
+ def untransform(target_account)
140
+ entries.each do |entry|
141
+ entry.fields['ownerId'] = target_account
142
+ entry.untransform user, target_account
143
+ end
144
+
145
+ @state = :untransformed
146
+ @account_id = target_account
147
+ true
148
+ end
149
+
150
+ private
151
+
152
+ def load_json_or_error(file)
153
+ Oj.load File.read file
154
+ rescue Oj::ParseError => exception
155
+ raise "#{exception.message.split(' [').first}: #{file}"
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,144 @@
1
+ module Cts
2
+ module Mpx
3
+ module Aci
4
+ # contains the logic to transform/untransform an entry.
5
+ module Transformations
6
+ module_function
7
+
8
+ # Transform a reference into a transformed_reference
9
+ # @param [User] user that will make the service calls.
10
+ # @param [String] original_account to do the transformation from
11
+ # @param [String] reference to transform to a transformed reference
12
+ # @return [String] transformed_reference
13
+ def transform_reference(reference: nil, user: nil, original_account: nil)
14
+ return "urn:cts:aci:target-account" if reference == original_account
15
+ return reference unless Validators.reference? reference
16
+
17
+ service_info = Services.from_url reference
18
+ endpoint = service_info[:endpoint]
19
+ service = service_info[:service]
20
+ return reference if service.start_with? "User Data Service"
21
+
22
+ response = Services::Data.get user: user, service: service, endpoint: endpoint, ids: reference.split('/').last
23
+
24
+ return "urn:cts:aci:no-id-found" unless (entry = response.data["entries"].first)
25
+
26
+ return "urn:cts:aci:no-guid-found" unless (guid = entry["guid"])
27
+
28
+ "urn:cts:aci:#{URI.encode_www_form_component(service)}:#{endpoint}:#{entry['ownerId'].split('/').last}:#{guid}"
29
+ end
30
+
31
+ # Transform a field reference into a transformed_field_reference
32
+ # @param [User] user that will make the service calls.
33
+ # @param [String] field_reference to transform to a transformed field reference
34
+ # @return [String] transformed_field_reference
35
+ def transform_field_reference(field_reference: nil, user: nil)
36
+ return field_reference unless Validators.field_reference? field_reference
37
+
38
+ service_info = Services.from_url field_reference
39
+ endpoint = service_info[:endpoint]
40
+ service = service_info[:service]
41
+ response = Services::Data.get user: user, service: service, endpoint: endpoint, ids: field_reference.split('/').last, fields: 'ownerId,fieldName,namespace'
42
+ return "urn:cts:aci:no-id-found" unless (entry = response.data["entries"].first)
43
+ return "urn:cts:aci:no-qualified-field-name-found" unless entry["fieldName"]
44
+
45
+ namespace = response.data['namespace']
46
+ owner_id = entry['ownerId'].split('/').last
47
+ "urn:cts:aci:#{URI.encode_www_form_component(service)}:#{endpoint}:#{owner_id}:#{namespace}$#{entry['fieldName']}"
48
+ end
49
+
50
+ # Untransform a transformed_reference into a reference
51
+ # @param [User] user that will make the service calls.
52
+ # @param [String] target_account to do the transformation from
53
+ # @param [String] transformed_reference to transform to a reference
54
+ # @return [String] reference
55
+ def untransform_reference(transformed_reference: nil, user: nil, target_account: nil)
56
+ return target_account if transformed_reference == "urn:cts:aci:target-account"
57
+ return transformed_reference unless Validators.transformed_reference? transformed_reference
58
+
59
+ parts = transformed_reference.split ':'
60
+ endpoint = parts[4]
61
+ service = parts[3]
62
+ guid = URI.decode_www_form_component(parts[6])
63
+ owner_id = "http://access.auth.theplatform.com/data/Account/#{parts[5]}"
64
+
65
+ response = Services::Data.get user: user,
66
+ service: service,
67
+ endpoint: endpoint,
68
+ ids: transformed_reference.split('/').last,
69
+ query: { byGuid: guid, ownerId: owner_id }
70
+
71
+ raise "could not find an entry by guid" unless (entry = response.data["entries"].first)
72
+ raise "service returned too many entries on guid" if response.data["entries"].count > 1
73
+
74
+ entry['id']
75
+ end
76
+
77
+ # Untransform a transformed_field_reference into a field reference
78
+ # @param [User] user that will make the service calls.
79
+ # @param [String] transformed_field_reference to transform to a reference
80
+ # @return [String] field_reference
81
+ def untransform_field_reference(transformed_field_reference: nil, user: nil, target_account: nil)
82
+ return transformed_field_reference unless Validators.transformed_field_reference? transformed_field_reference
83
+
84
+ parts = transformed_field_reference.split ':'
85
+ endpoint = parts[4]
86
+ service = parts[3]
87
+ qualified_field_name = URI.decode_www_form_component(parts[6])
88
+ owner_id = "http://access.auth.theplatform.com/data/Account/#{parts[5]}"
89
+
90
+ response = Services::Data.get user: user,
91
+ service: service,
92
+ endpoint: endpoint,
93
+ query: { byQualifiedFieldName: qualified_field_name, ownerId: owner_id }
94
+
95
+ raise "could not find an entry by qualified field name" unless (entry = response.data["entries"].first)
96
+ raise "service returned too many entries on qualified field name" if response.data["entries"].count > 1
97
+
98
+ entry['id']
99
+ end
100
+
101
+ def traverse_for(hash, direction, &block)
102
+ id = hash['id']
103
+ output = hash.reject { |k, _v| k == 'id' }
104
+ output = Transformations.send :traverse_hash, Oj.load(Oj.dump(output)), direction, &block
105
+ { "id" => id }.merge(output)
106
+ end
107
+
108
+ # private module method, not explicitly covered
109
+ def traverse_hash(entry, direction, &block)
110
+ entry.each do |field, value|
111
+ case value
112
+ when String
113
+ entry[field] = block.yield field, value if direction == :transform &&
114
+ Validators.reference?(value)
115
+ entry[field] = block.yield field, value if direction == :untransform &&
116
+ Validators.transformed_reference?(value)
117
+ when Array
118
+ entry[field] = traverse_array field, value, direction, &block
119
+ when Hash
120
+ entry[field] = traverse_hash value, direction, &block
121
+ end
122
+ end
123
+ end
124
+
125
+ # private module method, not explicitly covered
126
+ def traverse_array(field, value, direction, &block)
127
+ if value.map(&:class).uniq.first == String
128
+ value.map do |v|
129
+ if Validators.reference?(v) || Validators.transformed_reference?(v)
130
+ block.yield field, v
131
+ else
132
+ v
133
+ end
134
+ end
135
+ elsif value.map(&:class).uniq.first == Hash
136
+ value.map { |v| traverse_hash v, direction, &block }
137
+ else
138
+ value
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,114 @@
1
+ module Cts
2
+ module Mpx
3
+ module Aci
4
+ # Wireline Validators
5
+ module Validators
6
+ module_function
7
+
8
+ # Test if a string is a reference or not
9
+ # @param [String] uri to test if it is a reference or not
10
+ # @return [Boolean]
11
+ def reference?(uri)
12
+ Cts::Mpx::Validators.reference?(uri)
13
+ end
14
+
15
+ def field_reference?(uri)
16
+ begin
17
+ ref = URI.parse uri
18
+ rescue URI::InvalidURIError
19
+ return false
20
+ end
21
+
22
+ return false if ref.host == 'web.theplatform.com'
23
+ return false unless ref.scheme == "http" || ref.scheme == "https"
24
+ return false unless ref.host.end_with? ".theplatform.com"
25
+ return false unless ref.path =~ /Field\/\d+/
26
+ return false if ref.path =~ /\/\D+$/
27
+
28
+ true
29
+ end
30
+
31
+ # Test if a string is a transformed_reference or not
32
+ # @param [String] string to check if it is a transformed_reference
33
+ # @return [Boolean]
34
+ def transformed_reference?(string)
35
+ return true if [
36
+ 'urn:cts:aci:target-account',
37
+ 'urn:cts:aci:no-id-found',
38
+ 'urn:cts:aci:no-guid-found',
39
+ 'urn:cts:aci:no-custom-field-found'
40
+ ].include? string
41
+
42
+ urn_regex = /^(?i:urn:(?!urn:)([cC][tT][sS]):(?<nss>(?:[a-z0-9()\/+,-.:=@;$_!*']|%[0-9a-f]{2})+))$/
43
+
44
+ return false unless urn_regex =~ string
45
+
46
+ nss = urn_regex.match(string)["nss"]
47
+ segments = nss.split(":")
48
+
49
+ return false unless /^([aA][cC][iI])$/ =~ segments[0]
50
+
51
+ begin
52
+ service = Services[URI.decode_www_form_component(segments[1])]
53
+ rescue RuntimeError
54
+ return false
55
+ end
56
+
57
+ return false unless service.endpoints.include? segments[2].gsub('Field', '/Field')
58
+ return false unless /\A\d+\z/ =~ segments[3]
59
+
60
+ true
61
+ end
62
+
63
+ # Test if a string is a transformed_field_reference or not
64
+ # @param [String] string to check if it is a transformed_field_reference
65
+ # @return [Boolean]
66
+ def transformed_field_reference?(string)
67
+ return false unless transformed_reference? string
68
+ return false unless /Field:\d*:.*$/.match? string
69
+
70
+ true
71
+ end
72
+
73
+ # test if a directory is an image_directory
74
+ # @param [String] image_directory to check if it is a transformed_reference
75
+ # @return [Boolean]
76
+ def image_directory?(image_directory)
77
+ return false unless File.exist? "#{image_directory}/info.json"
78
+
79
+ true
80
+ end
81
+
82
+ # test if a image will depoly
83
+ # @param [Tasks::Image] image to check if it is a transformed_reference
84
+ # @return [Boolean]
85
+ def image_deployable?(image)
86
+ return false if image.state == :transformed || image.state == 'transformed'
87
+ return false if image.entries.collection.empty?
88
+
89
+ image.entries.each do |entry|
90
+ Transformations.traverse_for(entry.to_h, :untransform) do |_k, v|
91
+ return false if v =~ /^urn:cts:aci:no-(gu)?id-found$/
92
+ end
93
+ end
94
+ true
95
+ end
96
+
97
+ # Test if a string is a transformed_reference or not
98
+ # @param [String] filename to check if it is a valid info.json or not
99
+ # @return [Boolean]
100
+ def info_file?(filename)
101
+ raise "could not find an info.json" unless File.exist? filename
102
+
103
+ begin
104
+ Oj.load(File.read(filename))
105
+ rescue Oj::ParseError
106
+ return false
107
+ end
108
+
109
+ true
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end