cts-mpx-aci 2.0.0

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